diff options
-rw-r--r-- | src/lib/api/index.ts | 23 | ||||
-rw-r--r-- | src/view/com/composer/Composer.tsx | 61 | ||||
-rw-r--r-- | src/view/com/composer/photos/Gallery.tsx | 4 | ||||
-rw-r--r-- | src/view/com/composer/state/composer.ts | 96 |
4 files changed, 126 insertions, 58 deletions
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index f727aa4ca..46dbd1e66 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -30,14 +30,18 @@ import { createThreadgateRecord, threadgateAllowUISettingToAllowRecordValue, } from '#/state/queries/threadgate' -import {ComposerDraft, EmbedDraft} from '#/view/com/composer/state/composer' +import { + EmbedDraft, + PostDraft, + ThreadDraft, +} from '#/view/com/composer/state/composer' import {createGIFDescription} from '../gif-alt-text' import {uploadBlob} from './upload-blob' export {uploadBlob} interface PostOpts { - draft: ComposerDraft + thread: ThreadDraft replyTo?: string onStateChange?: (state: string) => void langs?: string[] @@ -48,7 +52,8 @@ export async function post( queryClient: QueryClient, opts: PostOpts, ) { - const draft = opts.draft + const thread = opts.thread + const draft = thread.posts[0] // TODO: Support threads. opts.onStateChange?.(t`Processing...`) // NB -- Do not await anything here to avoid waterfalls! @@ -111,11 +116,11 @@ export async function post( } // Create threadgate record - if (draft.threadgate.some(tg => tg.type !== 'everybody')) { + if (thread.threadgate.some(tg => tg.type !== 'everybody')) { const record = createThreadgateRecord({ createdAt: date, post: uri, - allow: threadgateAllowUISettingToAllowRecordValue(draft.threadgate), + allow: threadgateAllowUISettingToAllowRecordValue(thread.threadgate), }) writes.push({ @@ -128,11 +133,11 @@ export async function post( // Create postgate record if ( - draft.postgate.embeddingRules?.length || - draft.postgate.detachedEmbeddingUris?.length + thread.postgate.embeddingRules?.length || + thread.postgate.detachedEmbeddingUris?.length ) { const record: AppBskyFeedPostgate.Record = { - ...draft.postgate, + ...thread.postgate, $type: 'app.bsky.feed.postgate', createdAt: date, post: uri, @@ -198,7 +203,7 @@ async function resolveReply(agent: BskyAgent, replyTo: string) { async function resolveEmbed( agent: BskyAgent, queryClient: QueryClient, - draft: ComposerDraft, + draft: PostDraft, onStateChange: ((state: string) => void) | undefined, ): Promise< | AppBskyEmbedImages.Main diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 8fdc62bc3..214f3605e 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -114,11 +114,13 @@ import {Text as NewText} from '#/components/Typography' import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' import { ComposerAction, - ComposerDraft, composerReducer, createComposerState, EmbedDraft, MAX_IMAGES, + PostAction, + PostDraft, + ThreadDraft, } from './state/composer' import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video' @@ -161,11 +163,21 @@ export const ComposePost = ({ const [publishingStage, setPublishingStage] = useState('') const [error, setError] = useState('') - const [draft, dispatch] = useReducer( + const [composerState, composerDispatch] = useReducer( composerReducer, {initImageUris, initQuoteUri: initQuote?.uri, initText, initMention}, createComposerState, ) + + // TODO: Display drafts for other posts in the thread. + const draft = composerState.thread.posts[composerState.activePostIndex] + const dispatch = useCallback((postAction: PostAction) => { + composerDispatch({ + type: 'update_post', + postAction, + }) + }, []) + const richtext = draft.richtext let quote: string | undefined if (draft.embed.quote) { @@ -207,7 +219,7 @@ export const ComposePost = ({ _, ) }, - [_, agent, currentDid], + [_, agent, currentDid, dispatch], ) // Whenever we receive an initial video uri, we should immediately run compression if necessary @@ -333,7 +345,7 @@ export const ComposePost = ({ try { postUri = ( await apilib.post(agent, queryClient, { - draft: draft, + thread: composerState.thread, replyTo: replyTo?.uri, onStateChange: setPublishingStage, langs: toPostLanguages(langPrefs.postLanguage), @@ -409,7 +421,7 @@ export const ComposePost = ({ [ _, agent, - draft, + composerState.thread, extLink, images, canPost, @@ -504,8 +516,9 @@ export const ComposePost = ({ <ComposerPills isReply={!!replyTo} - draft={draft} - dispatch={dispatch} + post={draft} + thread={composerState.thread} + dispatch={composerDispatch} bottomBarAnimatedStyle={bottomBarAnimatedStyle} /> @@ -543,8 +556,8 @@ function ComposerPost({ onError, onPublish, }: { - draft: ComposerDraft - dispatch: (action: ComposerAction) => void + draft: PostDraft + dispatch: (action: PostAction) => void textInput: React.Ref<TextInputRef> isReply: boolean canRemoveQuote: boolean @@ -736,7 +749,7 @@ function ComposerEmbeds({ canRemoveQuote, }: { embed: EmbedDraft - dispatch: (action: ComposerAction) => void + dispatch: (action: PostAction) => void clearVideo: () => void canRemoveQuote: boolean }) { @@ -850,19 +863,21 @@ function ComposerEmbeds({ function ComposerPills({ isReply, - draft, + thread, + post, dispatch, bottomBarAnimatedStyle, }: { isReply: boolean - draft: ComposerDraft + thread: ThreadDraft + post: PostDraft dispatch: (action: ComposerAction) => void bottomBarAnimatedStyle: StyleProp<ViewStyle> }) { const t = useTheme() - const media = draft.embed.media + const media = post.embed.media const hasMedia = media?.type === 'images' || media?.type === 'video' - const hasLink = !!draft.embed.link + const hasLink = !!post.embed.link // Don't render anything if no pills are going to be displayed if (isReply && !hasMedia && !hasLink) { @@ -879,11 +894,11 @@ function ComposerPills({ showsHorizontalScrollIndicator={false}> {isReply ? null : ( <ThreadgateBtn - postgate={draft.postgate} + postgate={thread.postgate} onChangePostgate={nextPostgate => { dispatch({type: 'update_postgate', postgate: nextPostgate}) }} - threadgateAllowUISettings={draft.threadgate} + threadgateAllowUISettings={thread.threadgate} onChangeThreadgateAllowUISettings={nextThreadgate => { dispatch({ type: 'update_threadgate', @@ -895,9 +910,15 @@ function ComposerPills({ )} {hasMedia || hasLink ? ( <LabelsBtn - labels={draft.labels} + labels={post.labels} onChange={nextLabels => { - dispatch({type: 'update_labels', labels: nextLabels}) + dispatch({ + type: 'update_post', + postAction: { + type: 'update_labels', + labels: nextLabels, + }, + }) }} /> ) : null} @@ -914,8 +935,8 @@ function ComposerFooter({ onError, onSelectVideo, }: { - draft: ComposerDraft - dispatch: (action: ComposerAction) => void + draft: PostDraft + dispatch: (action: PostAction) => void graphemeLength: number onEmojiButtonPress: () => void onError: (error: string) => void diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index 5ff7042bc..e65c5407a 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -21,7 +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/composer' +import {PostAction} from '../state/composer' import {EditImageDialog} from './EditImageDialog' import {ImageAltTextDialog} from './ImageAltTextDialog' @@ -29,7 +29,7 @@ const IMAGE_GAP = 8 interface GalleryProps { images: ComposerImage[] - dispatch: (action: ComposerAction) => void + dispatch: (action: PostAction) => void } export let Gallery = (props: GalleryProps): React.ReactNode => { diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts index 3c1edb6bd..958718eaf 100644 --- a/src/view/com/composer/state/composer.ts +++ b/src/view/com/composer/state/composer.ts @@ -47,19 +47,15 @@ export type EmbedDraft = { link: Link | undefined } -export type ComposerDraft = { +export type PostDraft = { richtext: RichText labels: SelfLabel[] - postgate: AppBskyFeedPostgate.Record - threadgate: ThreadgateAllowUISetting[] embed: EmbedDraft } -export type ComposerAction = +export type PostAction = | {type: 'update_richtext'; richtext: RichText} | {type: 'update_labels'; labels: SelfLabel[]} - | {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record} - | {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]} | {type: 'embed_add_images'; images: ComposerImage[]} | {type: 'embed_update_image'; image: ComposerImage} | {type: 'embed_remove_image'; image: ComposerImage} @@ -77,35 +73,76 @@ export type ComposerAction = | {type: 'embed_update_gif'; alt: string} | {type: 'embed_remove_gif'} +export type ThreadDraft = { + posts: PostDraft[] + postgate: AppBskyFeedPostgate.Record + threadgate: ThreadgateAllowUISetting[] +} + +export type ComposerState = { + thread: ThreadDraft + activePostIndex: number // TODO: Add actions to update this. +} + +export type ComposerAction = + | {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record} + | {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]} + | {type: 'update_post'; postAction: PostAction} + export const MAX_IMAGES = 4 export function composerReducer( - state: ComposerDraft, + state: ComposerState, action: ComposerAction, -): ComposerDraft { +): ComposerState { switch (action.type) { - case 'update_richtext': { + case 'update_postgate': { return { ...state, - richtext: action.richtext, + thread: { + ...state.thread, + postgate: action.postgate, + }, } } - case 'update_labels': { + case 'update_threadgate': { return { ...state, - labels: action.labels, + thread: { + ...state.thread, + threadgate: action.threadgate, + }, } } - case 'update_postgate': { + case 'update_post': { + const nextPosts = [...state.thread.posts] + nextPosts[state.activePostIndex] = postReducer( + state.thread.posts[state.activePostIndex], + action.postAction, + ) return { ...state, - postgate: action.postgate, + thread: { + ...state.thread, + posts: nextPosts, + }, } } - case 'update_threadgate': { + } +} + +function postReducer(state: PostDraft, action: PostAction): PostDraft { + switch (action.type) { + case 'update_richtext': { return { ...state, - threadgate: action.threadgate, + richtext: action.richtext, + } + } + case 'update_labels': { + return { + ...state, + labels: action.labels, } } case 'embed_add_images': { @@ -339,8 +376,6 @@ export function composerReducer( }, } } - default: - return state } } @@ -354,7 +389,7 @@ export function createComposerState({ initMention: string | undefined initImageUris: ComposerOpts['imageUris'] initQuoteUri: string | undefined -}): ComposerDraft { +}): ComposerState { let media: ImagesMedia | undefined if (initImageUris?.length) { media = { @@ -385,14 +420,21 @@ export function createComposerState({ : '', }) return { - richtext: initRichText, - labels: [], - postgate: createPostgateRecord({post: ''}), - threadgate: threadgateViewToAllowUISetting(undefined), - embed: { - quote, - media, - link: undefined, + activePostIndex: 0, + thread: { + posts: [ + { + richtext: initRichText, + labels: [], + embed: { + quote, + media, + link: undefined, + }, + }, + ], + postgate: createPostgateRecord({post: ''}), + threadgate: threadgateViewToAllowUISetting(undefined), }, } } |