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>
This commit is contained in:
dan
2024-10-02 00:08:46 +09:00
committed by GitHub
parent a7ee561e40
commit d2fd5589dc
4 changed files with 162 additions and 16 deletions
+2
View File
@@ -24,6 +24,7 @@ import {
threadgateAllowUISettingToAllowRecordValue, threadgateAllowUISettingToAllowRecordValue,
writeThreadgateRecord, writeThreadgateRecord,
} from '#/state/queries/threadgate' } from '#/state/queries/threadgate'
import {ComposerState} from '#/view/com/composer/state'
import {LinkMeta} from '../link-meta/link-meta' import {LinkMeta} from '../link-meta/link-meta'
import {uploadBlob} from './upload-blob' import {uploadBlob} from './upload-blob'
@@ -38,6 +39,7 @@ export interface ExternalEmbedDraft {
} }
interface PostOpts { interface PostOpts {
composerState: ComposerState // TODO: Not used yet.
rawText: string rawText: string
replyTo?: string replyTo?: string
quote?: { quote?: {
+23 -6
View File
@@ -3,6 +3,7 @@ import React, {
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
useMemo, useMemo,
useReducer,
useRef, useRef,
useState, useState,
} from 'react' } from 'react'
@@ -66,7 +67,7 @@ import {logger} from '#/logger'
import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection' import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
import {useDialogStateControlContext} from '#/state/dialogs' import {useDialogStateControlContext} from '#/state/dialogs'
import {emitPostCreated} from '#/state/events' import {emitPostCreated} from '#/state/events'
import {ComposerImage, createInitialImages, pasteImage} from '#/state/gallery' import {ComposerImage, pasteImage} from '#/state/gallery'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {useModals} from '#/state/modals' import {useModals} from '#/state/modals'
import {useRequireAltTextEnabled} from '#/state/preferences' 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 {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {Text as NewText} from '#/components/Typography' import {Text as NewText} from '#/components/Typography'
import {composerReducer, createComposerState} from './state'
const MAX_IMAGES = 4 const MAX_IMAGES = 4
@@ -126,6 +128,8 @@ type CancelRef = {
onPressCancel: () => void onPressCancel: () => void
} }
const NO_IMAGES: ComposerImage[] = []
type Props = ComposerOpts type Props = ComposerOpts
export const ComposePost = ({ export const ComposePost = ({
replyTo, replyTo,
@@ -213,9 +217,17 @@ export const ComposePost = ({
) )
const [postgate, setPostgate] = useState(createPostgateRecord({post: ''})) const [postgate, setPostgate] = useState(createPostgateRecord({post: ''}))
const [images, setImages] = useState<ComposerImage[]>(() => // TODO: Move more state here.
createInitialImages(initImageUris), 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(() => { const onClose = useCallback(() => {
closeComposer() closeComposer()
}, [closeComposer]) }, [closeComposer])
@@ -301,9 +313,12 @@ export const ComposePost = ({
const onImageAdd = useCallback( const onImageAdd = useCallback(
(next: ComposerImage[]) => { (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( const onPhotoPasted = useCallback(
@@ -374,6 +389,7 @@ export const ComposePost = ({
try { try {
postUri = ( postUri = (
await apilib.post(agent, { await apilib.post(agent, {
composerState, // TODO: not used yet.
rawText: richtext.text, rawText: richtext.text,
replyTo: replyTo?.uri, replyTo: replyTo?.uri,
images, images,
@@ -475,6 +491,7 @@ export const ComposePost = ({
_, _,
agent, agent,
captions, captions,
composerState,
extLink, extLink,
images, images,
graphemeLength, graphemeLength,
@@ -717,7 +734,7 @@ export const ComposePost = ({
/> />
</View> </View>
<Gallery images={images} onChange={setImages} /> <Gallery images={images} dispatch={dispatch} />
{images.length === 0 && extLink && ( {images.length === 0 && extLink && (
<View style={a.relative}> <View style={a.relative}>
<ExternalEmbed <ExternalEmbed
+6 -10
View File
@@ -21,6 +21,7 @@ import {ComposerImage, cropImage} from '#/state/gallery'
import {Text} from '#/view/com/util/text/Text' import {Text} from '#/view/com/util/text/Text'
import {useTheme} from '#/alf' import {useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {ComposerAction} from '../state'
import {EditImageDialog} from './EditImageDialog' import {EditImageDialog} from './EditImageDialog'
import {ImageAltTextDialog} from './ImageAltTextDialog' import {ImageAltTextDialog} from './ImageAltTextDialog'
@@ -28,7 +29,7 @@ const IMAGE_GAP = 8
interface GalleryProps { interface GalleryProps {
images: ComposerImage[] images: ComposerImage[]
onChange: (next: ComposerImage[]) => void dispatch: (action: ComposerAction) => void
} }
export let Gallery = (props: GalleryProps): React.ReactNode => { export let Gallery = (props: GalleryProps): React.ReactNode => {
@@ -56,7 +57,7 @@ interface GalleryInnerProps extends GalleryProps {
containerInfo: Dimensions containerInfo: Dimensions
} }
const GalleryInner = ({images, containerInfo, onChange}: GalleryInnerProps) => { const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const {altTextControlStyle, imageControlsStyle, imageStyle} = const {altTextControlStyle, imageControlsStyle, imageStyle} =
@@ -96,7 +97,7 @@ const GalleryInner = ({images, containerInfo, onChange}: GalleryInnerProps) => {
return images.length !== 0 ? ( return images.length !== 0 ? (
<> <>
<View testID="selectedPhotosView" style={styles.gallery}> <View testID="selectedPhotosView" style={styles.gallery}>
{images.map((image, index) => { {images.map(image => {
return ( return (
<GalleryItem <GalleryItem
key={image.source.id} key={image.source.id}
@@ -105,15 +106,10 @@ const GalleryInner = ({images, containerInfo, onChange}: GalleryInnerProps) => {
imageControlsStyle={imageControlsStyle} imageControlsStyle={imageControlsStyle}
imageStyle={imageStyle} imageStyle={imageStyle}
onChange={next => { onChange={next => {
onChange( dispatch({type: 'embed_update_image', image: next})
images.map(i => (i.source === image.source ? next : i)),
)
}} }}
onRemove={() => { onRemove={() => {
const next = images.slice() dispatch({type: 'embed_remove_image', image})
next.splice(index, 1)
onChange(next)
}} }}
/> />
) )
+131
View File
@@ -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,
},
}
}