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:
@@ -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?: {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user