Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a composer reducer and move image state there #5547

Merged
merged 4 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/lib/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -38,6 +39,7 @@ export interface ExternalEmbedDraft {
}

interface PostOpts {
composerState: ComposerState // TODO: Not used yet.
rawText: string
replyTo?: string
quote?: {
Expand Down
29 changes: 23 additions & 6 deletions src/view/com/composer/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, {
useEffect,
useImperativeHandle,
useMemo,
useReducer,
useRef,
useState,
} from 'react'
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -119,13 +120,16 @@ 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

type CancelRef = {
onPressCancel: () => void
}

const NO_IMAGES: ComposerImage[] = []

type Props = ComposerOpts
export const ComposePost = ({
replyTo,
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -475,6 +491,7 @@ export const ComposePost = ({
_,
agent,
captions,
composerState,
extLink,
images,
graphemeLength,
Expand Down Expand Up @@ -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
Expand Down
16 changes: 6 additions & 10 deletions src/view/com/composer/photos/Gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ 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'

const IMAGE_GAP = 8

interface GalleryProps {
images: ComposerImage[]
onChange: (next: ComposerImage[]) => void
dispatch: (action: ComposerAction) => void
}

export let Gallery = (props: GalleryProps): React.ReactNode => {
Expand Down Expand Up @@ -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} =
Expand Down Expand Up @@ -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}
Expand All @@ -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})
}}
/>
)
Expand Down
131 changes: 131 additions & 0 deletions src/view/com/composer/state.ts
Original file line number Diff line number Diff line change
@@ -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,
},
}
}
Loading