diff options
author | dan <dan.abramov@gmail.com> | 2024-10-02 00:08:46 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-10-02 00:08:46 +0900 |
commit | d2fd5589dc93831cda625ad91083b8b051878d39 (patch) | |
tree | 0bb56467eece49028d70ebe5402f4792630d433a /src | |
parent | a7ee561e4074f839f340c77a1f21c8f5657865c7 (diff) | |
download | voidsky-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.ts | 2 | ||||
-rw-r--r-- | src/view/com/composer/Composer.tsx | 29 | ||||
-rw-r--r-- | src/view/com/composer/photos/Gallery.tsx | 16 | ||||
-rw-r--r-- | src/view/com/composer/state.ts | 131 |
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, + }, + } +} |