about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/view/com/composer/Composer.tsx17
-rw-r--r--src/view/com/composer/state/composer.ts151
2 files changed, 151 insertions, 17 deletions
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index f4e290ca8..b5f2d84d4 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 = ({
                   <QuoteEmbed quote={quote} />
                 </View>
                 {quote.uri !== initQuote?.uri && (
-                  <QuoteX onRemove={() => setQuote(undefined)} />
+                  <QuoteX
+                    onRemove={() => {
+                      dispatch({type: 'embed_remove_quote'})
+                      setQuote(undefined)
+                    }}
+                  />
                 )}
               </View>
             ) : null}
diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts
index a23a5d8c8..769a0521d 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,
     },
   }
 }