about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2024-10-02 00:08:46 +0900
committerGitHub <noreply@github.com>2024-10-02 00:08:46 +0900
commitd2fd5589dc93831cda625ad91083b8b051878d39 (patch)
tree0bb56467eece49028d70ebe5402f4792630d433a /src
parenta7ee561e4074f839f340c77a1f21c8f5657865c7 (diff)
downloadvoidsky-d2fd5589dc93831cda625ad91083b8b051878d39.tar.zst
Introduce a composer reducer and move image state there (#5547)
* Add composer reducer

* Support adding images

Co-authored-by: Mary <git@mary.my.id>

* Support updating and deleting images

Co-authored-by: Mary <git@mary.my.id>

* Derive images state from composer state

Co-authored-by: Mary <git@mary.my.id>

---------

Co-authored-by: Mary <git@mary.my.id>
Diffstat (limited to 'src')
-rw-r--r--src/lib/api/index.ts2
-rw-r--r--src/view/com/composer/Composer.tsx29
-rw-r--r--src/view/com/composer/photos/Gallery.tsx16
-rw-r--r--src/view/com/composer/state.ts131
4 files changed, 162 insertions, 16 deletions
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts
index 08d4cb962..51bf51fff 100644
--- a/src/lib/api/index.ts
+++ b/src/lib/api/index.ts
@@ -24,6 +24,7 @@ import {
   threadgateAllowUISettingToAllowRecordValue,
   writeThreadgateRecord,
 } from '#/state/queries/threadgate'
+import {ComposerState} from '#/view/com/composer/state'
 import {LinkMeta} from '../link-meta/link-meta'
 import {uploadBlob} from './upload-blob'
 
@@ -38,6 +39,7 @@ export interface ExternalEmbedDraft {
 }
 
 interface PostOpts {
+  composerState: ComposerState // TODO: Not used yet.
   rawText: string
   replyTo?: string
   quote?: {
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index ade37af1b..f354f0f0d 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -3,6 +3,7 @@ import React, {
   useEffect,
   useImperativeHandle,
   useMemo,
+  useReducer,
   useRef,
   useState,
 } from 'react'
@@ -66,7 +67,7 @@ import {logger} from '#/logger'
 import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
 import {useDialogStateControlContext} from '#/state/dialogs'
 import {emitPostCreated} from '#/state/events'
-import {ComposerImage, createInitialImages, pasteImage} from '#/state/gallery'
+import {ComposerImage, pasteImage} from '#/state/gallery'
 import {useModalControls} from '#/state/modals'
 import {useModals} from '#/state/modals'
 import {useRequireAltTextEnabled} from '#/state/preferences'
@@ -119,6 +120,7 @@ import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons
 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
 import * as Prompt from '#/components/Prompt'
 import {Text as NewText} from '#/components/Typography'
+import {composerReducer, createComposerState} from './state'
 
 const MAX_IMAGES = 4
 
@@ -126,6 +128,8 @@ type CancelRef = {
   onPressCancel: () => void
 }
 
+const NO_IMAGES: ComposerImage[] = []
+
 type Props = ComposerOpts
 export const ComposePost = ({
   replyTo,
@@ -213,9 +217,17 @@ export const ComposePost = ({
     )
   const [postgate, setPostgate] = useState(createPostgateRecord({post: ''}))
 
-  const [images, setImages] = useState<ComposerImage[]>(() =>
-    createInitialImages(initImageUris),
+  // TODO: Move more state here.
+  const [composerState, dispatch] = useReducer(
+    composerReducer,
+    {initImageUris},
+    createComposerState,
   )
+  let images = NO_IMAGES
+  if (composerState.embed.media?.type === 'images') {
+    images = composerState.embed.media.images
+  }
+
   const onClose = useCallback(() => {
     closeComposer()
   }, [closeComposer])
@@ -301,9 +313,12 @@ export const ComposePost = ({
 
   const onImageAdd = useCallback(
     (next: ComposerImage[]) => {
-      setImages(prev => prev.concat(next.slice(0, MAX_IMAGES - prev.length)))
+      dispatch({
+        type: 'embed_add_images',
+        images: next,
+      })
     },
-    [setImages],
+    [dispatch],
   )
 
   const onPhotoPasted = useCallback(
@@ -374,6 +389,7 @@ export const ComposePost = ({
       try {
         postUri = (
           await apilib.post(agent, {
+            composerState, // TODO: not used yet.
             rawText: richtext.text,
             replyTo: replyTo?.uri,
             images,
@@ -475,6 +491,7 @@ export const ComposePost = ({
       _,
       agent,
       captions,
+      composerState,
       extLink,
       images,
       graphemeLength,
@@ -717,7 +734,7 @@ export const ComposePost = ({
             />
           </View>
 
-          <Gallery images={images} onChange={setImages} />
+          <Gallery images={images} dispatch={dispatch} />
           {images.length === 0 && extLink && (
             <View style={a.relative}>
               <ExternalEmbed
diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx
index 369f08d74..5692f3d2c 100644
--- a/src/view/com/composer/photos/Gallery.tsx
+++ b/src/view/com/composer/photos/Gallery.tsx
@@ -21,6 +21,7 @@ import {ComposerImage, cropImage} from '#/state/gallery'
 import {Text} from '#/view/com/util/text/Text'
 import {useTheme} from '#/alf'
 import * as Dialog from '#/components/Dialog'
+import {ComposerAction} from '../state'
 import {EditImageDialog} from './EditImageDialog'
 import {ImageAltTextDialog} from './ImageAltTextDialog'
 
@@ -28,7 +29,7 @@ const IMAGE_GAP = 8
 
 interface GalleryProps {
   images: ComposerImage[]
-  onChange: (next: ComposerImage[]) => void
+  dispatch: (action: ComposerAction) => void
 }
 
 export let Gallery = (props: GalleryProps): React.ReactNode => {
@@ -56,7 +57,7 @@ interface GalleryInnerProps extends GalleryProps {
   containerInfo: Dimensions
 }
 
-const GalleryInner = ({images, containerInfo, onChange}: GalleryInnerProps) => {
+const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
   const {isMobile} = useWebMediaQueries()
 
   const {altTextControlStyle, imageControlsStyle, imageStyle} =
@@ -96,7 +97,7 @@ const GalleryInner = ({images, containerInfo, onChange}: GalleryInnerProps) => {
   return images.length !== 0 ? (
     <>
       <View testID="selectedPhotosView" style={styles.gallery}>
-        {images.map((image, index) => {
+        {images.map(image => {
           return (
             <GalleryItem
               key={image.source.id}
@@ -105,15 +106,10 @@ const GalleryInner = ({images, containerInfo, onChange}: GalleryInnerProps) => {
               imageControlsStyle={imageControlsStyle}
               imageStyle={imageStyle}
               onChange={next => {
-                onChange(
-                  images.map(i => (i.source === image.source ? next : i)),
-                )
+                dispatch({type: 'embed_update_image', image: next})
               }}
               onRemove={() => {
-                const next = images.slice()
-                next.splice(index, 1)
-
-                onChange(next)
+                dispatch({type: 'embed_remove_image', image})
               }}
             />
           )
diff --git a/src/view/com/composer/state.ts b/src/view/com/composer/state.ts
new file mode 100644
index 000000000..5588de1aa
--- /dev/null
+++ b/src/view/com/composer/state.ts
@@ -0,0 +1,131 @@
+import {ComposerImage, createInitialImages} from '#/state/gallery'
+import {ComposerOpts} from '#/state/shell/composer'
+
+type PostRecord = {
+  uri: string
+}
+
+type ImagesMedia = {
+  type: 'images'
+  images: ComposerImage[]
+  labels: string[]
+}
+
+type ComposerEmbed = {
+  // TODO: Other record types.
+  record: PostRecord | undefined
+  // TODO: Other media types.
+  media: ImagesMedia | undefined
+}
+
+export type ComposerState = {
+  // TODO: Other draft data.
+  embed: ComposerEmbed
+}
+
+export type ComposerAction =
+  | {type: 'embed_add_images'; images: ComposerImage[]}
+  | {type: 'embed_update_image'; image: ComposerImage}
+  | {type: 'embed_remove_image'; image: ComposerImage}
+
+const MAX_IMAGES = 4
+
+export function composerReducer(
+  state: ComposerState,
+  action: ComposerAction,
+): ComposerState {
+  switch (action.type) {
+    case 'embed_add_images': {
+      const prevMedia = state.embed.media
+      let nextMedia = prevMedia
+      if (!prevMedia) {
+        nextMedia = {
+          type: 'images',
+          images: action.images.slice(0, MAX_IMAGES),
+          labels: [],
+        }
+      } else if (prevMedia.type === 'images') {
+        nextMedia = {
+          ...prevMedia,
+          images: [...prevMedia.images, ...action.images].slice(0, MAX_IMAGES),
+        }
+      }
+      return {
+        ...state,
+        embed: {
+          ...state.embed,
+          media: nextMedia,
+        },
+      }
+    }
+    case 'embed_update_image': {
+      const prevMedia = state.embed.media
+      if (prevMedia?.type === 'images') {
+        const updatedImage = action.image
+        const nextMedia = {
+          ...prevMedia,
+          images: prevMedia.images.map(img => {
+            if (img.source.id === updatedImage.source.id) {
+              return updatedImage
+            }
+            return img
+          }),
+        }
+        return {
+          ...state,
+          embed: {
+            ...state.embed,
+            media: nextMedia,
+          },
+        }
+      }
+      return state
+    }
+    case 'embed_remove_image': {
+      const prevMedia = state.embed.media
+      if (prevMedia?.type === 'images') {
+        const removedImage = action.image
+        let nextMedia: ImagesMedia | undefined = {
+          ...prevMedia,
+          images: prevMedia.images.filter(img => {
+            return img.source.id !== removedImage.source.id
+          }),
+        }
+        if (nextMedia.images.length === 0) {
+          nextMedia = undefined
+        }
+        return {
+          ...state,
+          embed: {
+            ...state.embed,
+            media: nextMedia,
+          },
+        }
+      }
+      return state
+    }
+    default:
+      return state
+  }
+}
+
+export function createComposerState({
+  initImageUris,
+}: {
+  initImageUris: ComposerOpts['imageUris']
+}): ComposerState {
+  let media: ImagesMedia | undefined
+  if (initImageUris?.length) {
+    media = {
+      type: 'images',
+      images: createInitialImages(initImageUris),
+      labels: [],
+    }
+  }
+  return {
+    embed: {
+      record: undefined,
+      media,
+    },
+  }
+}