diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index f4e290ca8d..b5f2d84d4f 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -190,7 +190,7 @@ export const ComposePost = ({ // TODO: Move more state here. const [composerState, dispatch] = useReducer( composerReducer, - {initImageUris}, + {initImageUris, initQuoteUri: initQuote?.uri}, createComposerState, ) @@ -337,6 +337,7 @@ export const ComposePost = ({ const onNewLink = useCallback( (uri: string) => { + dispatch({type: 'embed_add_uri', uri}) if (extLink != null) return setExtLink({uri, isLoading: true}) }, @@ -582,6 +583,7 @@ export const ComposePost = ({ const onSelectGif = useCallback( (gif: Gif) => { + dispatch({type: 'embed_add_gif', gif}) setExtLink({ uri: `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[1]}&ww=${gif.media_formats.gif.dims[0]}`, isLoading: true, @@ -600,6 +602,7 @@ export const ComposePost = ({ const handleChangeGifAltText = useCallback( (altText: string) => { + dispatch({type: 'embed_update_gif', alt: altText}) setExtLink(ext => ext && ext.meta ? { @@ -770,6 +773,11 @@ export const ComposePost = ({ link={extLink} gif={extGif} onRemove={() => { + if (extGif) { + dispatch({type: 'embed_remove_gif'}) + } else { + dispatch({type: 'embed_remove_link'}) + } setExtLink(undefined) setExtGif(undefined) }} @@ -818,7 +826,12 @@ export const ComposePost = ({ {quote.uri !== initQuote?.uri && ( - setQuote(undefined)} /> + { + dispatch({type: 'embed_remove_quote'}) + setQuote(undefined) + }} + /> )} ) : null} diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts index a23a5d8c86..769a0521d4 100644 --- a/src/view/com/composer/state/composer.ts +++ b/src/view/com/composer/state/composer.ts @@ -1,17 +1,14 @@ import {ImagePickerAsset} from 'expo-image-picker' +import {isBskyPostUrl} from '#/lib/strings/url-helpers' import {ComposerImage, createInitialImages} from '#/state/gallery' +import {Gif} from '#/state/queries/tenor' import {ComposerOpts} from '#/state/shell/composer' import {createVideoState, VideoAction, videoReducer, VideoState} from './video' -type PostRecord = { - uri: string -} - type ImagesMedia = { type: 'images' images: ComposerImage[] - labels: string[] } type VideoMedia = { @@ -19,16 +16,30 @@ type VideoMedia = { video: VideoState } -type ComposerEmbed = { - // TODO: Other record types. - record: PostRecord | undefined - // TODO: Other media types. - media: ImagesMedia | VideoMedia | undefined +type GifMedia = { + type: 'gif' + gif: Gif + alt: string +} + +type Link = { + type: 'link' + uri: string +} + +// This structure doesn't exactly correspond to the data model. +// Instead, it maps to how the UI is organized, and how we present a post. +type EmbedDraft = { + // We'll always submit quote and actual media (images, video, gifs) chosen by the user. + quote: Link | undefined + media: ImagesMedia | VideoMedia | GifMedia | undefined + // This field may end up ignored if we have more important things to display than a link card: + link: Link | undefined } export type ComposerState = { // TODO: Other draft data. - embed: ComposerEmbed + embed: EmbedDraft } export type ComposerAction = @@ -42,6 +53,12 @@ export type ComposerAction = } | {type: 'embed_remove_video'} | {type: 'embed_update_video'; videoAction: VideoAction} + | {type: 'embed_add_uri'; uri: string} + | {type: 'embed_remove_quote'} + | {type: 'embed_remove_link'} + | {type: 'embed_add_gif'; gif: Gif} + | {type: 'embed_update_gif'; alt: string} + | {type: 'embed_remove_gif'} const MAX_IMAGES = 4 @@ -60,7 +77,6 @@ export function composerReducer( nextMedia = { type: 'images', images: action.images.slice(0, MAX_IMAGES), - labels: [], } } else if (prevMedia.type === 'images') { nextMedia = { @@ -171,6 +187,102 @@ export function composerReducer( }, } } + case 'embed_add_uri': { + const prevQuote = state.embed.quote + const prevLink = state.embed.link + let nextQuote = prevQuote + let nextLink = prevLink + if (isBskyPostUrl(action.uri)) { + if (!prevQuote) { + nextQuote = { + type: 'link', + uri: action.uri, + } + } + } else { + if (!prevLink) { + nextLink = { + type: 'link', + uri: action.uri, + } + } + } + return { + ...state, + embed: { + ...state.embed, + quote: nextQuote, + link: nextLink, + }, + } + } + case 'embed_remove_link': { + return { + ...state, + embed: { + ...state.embed, + link: undefined, + }, + } + } + case 'embed_remove_quote': { + return { + ...state, + embed: { + ...state.embed, + quote: undefined, + }, + } + } + case 'embed_add_gif': { + const prevMedia = state.embed.media + let nextMedia = prevMedia + if (!prevMedia) { + nextMedia = { + type: 'gif', + gif: action.gif, + alt: '', + } + } + return { + ...state, + embed: { + ...state.embed, + media: nextMedia, + }, + } + } + case 'embed_update_gif': { + const prevMedia = state.embed.media + let nextMedia = prevMedia + if (prevMedia?.type === 'gif') { + nextMedia = { + ...prevMedia, + alt: action.alt, + } + } + return { + ...state, + embed: { + ...state.embed, + media: nextMedia, + }, + } + } + case 'embed_remove_gif': { + const prevMedia = state.embed.media + let nextMedia = prevMedia + if (prevMedia?.type === 'gif') { + nextMedia = undefined + } + return { + ...state, + embed: { + ...state.embed, + media: nextMedia, + }, + } + } default: return state } @@ -178,22 +290,31 @@ export function composerReducer( export function createComposerState({ initImageUris, + initQuoteUri, }: { initImageUris: ComposerOpts['imageUris'] + initQuoteUri: string | undefined }): ComposerState { let media: ImagesMedia | undefined if (initImageUris?.length) { media = { type: 'images', images: createInitialImages(initImageUris), - labels: [], } } - // TODO: initial video. + let quote: Link | undefined + if (initQuoteUri) { + quote = { + type: 'link', + uri: initQuoteUri, + } + } + // TODO: Other initial content. return { embed: { - record: undefined, + quote, media, + link: undefined, }, } }