Refactor composer state for threads (#5945)

* Refactor composer state for threads

* Remove unnecessary default case

TS can see it's exhaustive.
This commit is contained in:
dan
2024-10-29 20:27:44 +00:00
committed by GitHub
parent 3bf91eb814
commit bab44a5a13
4 changed files with 130 additions and 62 deletions
+14 -9
View File
@@ -30,14 +30,18 @@ import {
createThreadgateRecord, createThreadgateRecord,
threadgateAllowUISettingToAllowRecordValue, threadgateAllowUISettingToAllowRecordValue,
} from '#/state/queries/threadgate' } 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 {createGIFDescription} from '../gif-alt-text'
import {uploadBlob} from './upload-blob' import {uploadBlob} from './upload-blob'
export {uploadBlob} export {uploadBlob}
interface PostOpts { interface PostOpts {
draft: ComposerDraft thread: ThreadDraft
replyTo?: string replyTo?: string
onStateChange?: (state: string) => void onStateChange?: (state: string) => void
langs?: string[] langs?: string[]
@@ -48,7 +52,8 @@ export async function post(
queryClient: QueryClient, queryClient: QueryClient,
opts: PostOpts, opts: PostOpts,
) { ) {
const draft = opts.draft const thread = opts.thread
const draft = thread.posts[0] // TODO: Support threads.
opts.onStateChange?.(t`Processing...`) opts.onStateChange?.(t`Processing...`)
// NB -- Do not await anything here to avoid waterfalls! // NB -- Do not await anything here to avoid waterfalls!
@@ -111,11 +116,11 @@ export async function post(
} }
// Create threadgate record // Create threadgate record
if (draft.threadgate.some(tg => tg.type !== 'everybody')) { if (thread.threadgate.some(tg => tg.type !== 'everybody')) {
const record = createThreadgateRecord({ const record = createThreadgateRecord({
createdAt: date, createdAt: date,
post: uri, post: uri,
allow: threadgateAllowUISettingToAllowRecordValue(draft.threadgate), allow: threadgateAllowUISettingToAllowRecordValue(thread.threadgate),
}) })
writes.push({ writes.push({
@@ -128,11 +133,11 @@ export async function post(
// Create postgate record // Create postgate record
if ( if (
draft.postgate.embeddingRules?.length || thread.postgate.embeddingRules?.length ||
draft.postgate.detachedEmbeddingUris?.length thread.postgate.detachedEmbeddingUris?.length
) { ) {
const record: AppBskyFeedPostgate.Record = { const record: AppBskyFeedPostgate.Record = {
...draft.postgate, ...thread.postgate,
$type: 'app.bsky.feed.postgate', $type: 'app.bsky.feed.postgate',
createdAt: date, createdAt: date,
post: uri, post: uri,
@@ -198,7 +203,7 @@ async function resolveReply(agent: BskyAgent, replyTo: string) {
async function resolveEmbed( async function resolveEmbed(
agent: BskyAgent, agent: BskyAgent,
queryClient: QueryClient, queryClient: QueryClient,
draft: ComposerDraft, draft: PostDraft,
onStateChange: ((state: string) => void) | undefined, onStateChange: ((state: string) => void) | undefined,
): Promise< ): Promise<
| AppBskyEmbedImages.Main | AppBskyEmbedImages.Main
+41 -20
View File
@@ -114,11 +114,13 @@ import {Text as NewText} from '#/components/Typography'
import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
import { import {
ComposerAction, ComposerAction,
ComposerDraft,
composerReducer, composerReducer,
createComposerState, createComposerState,
EmbedDraft, EmbedDraft,
MAX_IMAGES, MAX_IMAGES,
PostAction,
PostDraft,
ThreadDraft,
} from './state/composer' } from './state/composer'
import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video' import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video'
@@ -161,11 +163,21 @@ export const ComposePost = ({
const [publishingStage, setPublishingStage] = useState('') const [publishingStage, setPublishingStage] = useState('')
const [error, setError] = useState('') const [error, setError] = useState('')
const [draft, dispatch] = useReducer( const [composerState, composerDispatch] = useReducer(
composerReducer, composerReducer,
{initImageUris, initQuoteUri: initQuote?.uri, initText, initMention}, {initImageUris, initQuoteUri: initQuote?.uri, initText, initMention},
createComposerState, 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 const richtext = draft.richtext
let quote: string | undefined let quote: string | undefined
if (draft.embed.quote) { 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 // Whenever we receive an initial video uri, we should immediately run compression if necessary
@@ -333,7 +345,7 @@ export const ComposePost = ({
try { try {
postUri = ( postUri = (
await apilib.post(agent, queryClient, { await apilib.post(agent, queryClient, {
draft: draft, thread: composerState.thread,
replyTo: replyTo?.uri, replyTo: replyTo?.uri,
onStateChange: setPublishingStage, onStateChange: setPublishingStage,
langs: toPostLanguages(langPrefs.postLanguage), langs: toPostLanguages(langPrefs.postLanguage),
@@ -409,7 +421,7 @@ export const ComposePost = ({
[ [
_, _,
agent, agent,
draft, composerState.thread,
extLink, extLink,
images, images,
canPost, canPost,
@@ -504,8 +516,9 @@ export const ComposePost = ({
<ComposerPills <ComposerPills
isReply={!!replyTo} isReply={!!replyTo}
draft={draft} post={draft}
dispatch={dispatch} thread={composerState.thread}
dispatch={composerDispatch}
bottomBarAnimatedStyle={bottomBarAnimatedStyle} bottomBarAnimatedStyle={bottomBarAnimatedStyle}
/> />
@@ -543,8 +556,8 @@ function ComposerPost({
onError, onError,
onPublish, onPublish,
}: { }: {
draft: ComposerDraft draft: PostDraft
dispatch: (action: ComposerAction) => void dispatch: (action: PostAction) => void
textInput: React.Ref<TextInputRef> textInput: React.Ref<TextInputRef>
isReply: boolean isReply: boolean
canRemoveQuote: boolean canRemoveQuote: boolean
@@ -736,7 +749,7 @@ function ComposerEmbeds({
canRemoveQuote, canRemoveQuote,
}: { }: {
embed: EmbedDraft embed: EmbedDraft
dispatch: (action: ComposerAction) => void dispatch: (action: PostAction) => void
clearVideo: () => void clearVideo: () => void
canRemoveQuote: boolean canRemoveQuote: boolean
}) { }) {
@@ -850,19 +863,21 @@ function ComposerEmbeds({
function ComposerPills({ function ComposerPills({
isReply, isReply,
draft, thread,
post,
dispatch, dispatch,
bottomBarAnimatedStyle, bottomBarAnimatedStyle,
}: { }: {
isReply: boolean isReply: boolean
draft: ComposerDraft thread: ThreadDraft
post: PostDraft
dispatch: (action: ComposerAction) => void dispatch: (action: ComposerAction) => void
bottomBarAnimatedStyle: StyleProp<ViewStyle> bottomBarAnimatedStyle: StyleProp<ViewStyle>
}) { }) {
const t = useTheme() const t = useTheme()
const media = draft.embed.media const media = post.embed.media
const hasMedia = media?.type === 'images' || media?.type === 'video' 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 // Don't render anything if no pills are going to be displayed
if (isReply && !hasMedia && !hasLink) { if (isReply && !hasMedia && !hasLink) {
@@ -879,11 +894,11 @@ function ComposerPills({
showsHorizontalScrollIndicator={false}> showsHorizontalScrollIndicator={false}>
{isReply ? null : ( {isReply ? null : (
<ThreadgateBtn <ThreadgateBtn
postgate={draft.postgate} postgate={thread.postgate}
onChangePostgate={nextPostgate => { onChangePostgate={nextPostgate => {
dispatch({type: 'update_postgate', postgate: nextPostgate}) dispatch({type: 'update_postgate', postgate: nextPostgate})
}} }}
threadgateAllowUISettings={draft.threadgate} threadgateAllowUISettings={thread.threadgate}
onChangeThreadgateAllowUISettings={nextThreadgate => { onChangeThreadgateAllowUISettings={nextThreadgate => {
dispatch({ dispatch({
type: 'update_threadgate', type: 'update_threadgate',
@@ -895,9 +910,15 @@ function ComposerPills({
)} )}
{hasMedia || hasLink ? ( {hasMedia || hasLink ? (
<LabelsBtn <LabelsBtn
labels={draft.labels} labels={post.labels}
onChange={nextLabels => { onChange={nextLabels => {
dispatch({type: 'update_labels', labels: nextLabels}) dispatch({
type: 'update_post',
postAction: {
type: 'update_labels',
labels: nextLabels,
},
})
}} }}
/> />
) : null} ) : null}
@@ -914,8 +935,8 @@ function ComposerFooter({
onError, onError,
onSelectVideo, onSelectVideo,
}: { }: {
draft: ComposerDraft draft: PostDraft
dispatch: (action: ComposerAction) => void dispatch: (action: PostAction) => void
graphemeLength: number graphemeLength: number
onEmojiButtonPress: () => void onEmojiButtonPress: () => void
onError: (error: string) => void onError: (error: string) => void
+2 -2
View File
@@ -21,7 +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/composer' import {PostAction} from '../state/composer'
import {EditImageDialog} from './EditImageDialog' import {EditImageDialog} from './EditImageDialog'
import {ImageAltTextDialog} from './ImageAltTextDialog' import {ImageAltTextDialog} from './ImageAltTextDialog'
@@ -29,7 +29,7 @@ const IMAGE_GAP = 8
interface GalleryProps { interface GalleryProps {
images: ComposerImage[] images: ComposerImage[]
dispatch: (action: ComposerAction) => void dispatch: (action: PostAction) => void
} }
export let Gallery = (props: GalleryProps): React.ReactNode => { export let Gallery = (props: GalleryProps): React.ReactNode => {
+67 -25
View File
@@ -47,19 +47,15 @@ export type EmbedDraft = {
link: Link | undefined link: Link | undefined
} }
export type ComposerDraft = { export type PostDraft = {
richtext: RichText richtext: RichText
labels: SelfLabel[] labels: SelfLabel[]
postgate: AppBskyFeedPostgate.Record
threadgate: ThreadgateAllowUISetting[]
embed: EmbedDraft embed: EmbedDraft
} }
export type ComposerAction = export type PostAction =
| {type: 'update_richtext'; richtext: RichText} | {type: 'update_richtext'; richtext: RichText}
| {type: 'update_labels'; labels: SelfLabel[]} | {type: 'update_labels'; labels: SelfLabel[]}
| {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record}
| {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]}
| {type: 'embed_add_images'; images: ComposerImage[]} | {type: 'embed_add_images'; images: ComposerImage[]}
| {type: 'embed_update_image'; image: ComposerImage} | {type: 'embed_update_image'; image: ComposerImage}
| {type: 'embed_remove_image'; image: ComposerImage} | {type: 'embed_remove_image'; image: ComposerImage}
@@ -77,12 +73,65 @@ export type ComposerAction =
| {type: 'embed_update_gif'; alt: string} | {type: 'embed_update_gif'; alt: string}
| {type: 'embed_remove_gif'} | {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 const MAX_IMAGES = 4
export function composerReducer( export function composerReducer(
state: ComposerDraft, state: ComposerState,
action: ComposerAction, action: ComposerAction,
): ComposerDraft { ): ComposerState {
switch (action.type) {
case 'update_postgate': {
return {
...state,
thread: {
...state.thread,
postgate: action.postgate,
},
}
}
case 'update_threadgate': {
return {
...state,
thread: {
...state.thread,
threadgate: action.threadgate,
},
}
}
case 'update_post': {
const nextPosts = [...state.thread.posts]
nextPosts[state.activePostIndex] = postReducer(
state.thread.posts[state.activePostIndex],
action.postAction,
)
return {
...state,
thread: {
...state.thread,
posts: nextPosts,
},
}
}
}
}
function postReducer(state: PostDraft, action: PostAction): PostDraft {
switch (action.type) { switch (action.type) {
case 'update_richtext': { case 'update_richtext': {
return { return {
@@ -96,18 +145,6 @@ export function composerReducer(
labels: action.labels, labels: action.labels,
} }
} }
case 'update_postgate': {
return {
...state,
postgate: action.postgate,
}
}
case 'update_threadgate': {
return {
...state,
threadgate: action.threadgate,
}
}
case 'embed_add_images': { case 'embed_add_images': {
if (action.images.length === 0) { if (action.images.length === 0) {
return state return state
@@ -339,8 +376,6 @@ export function composerReducer(
}, },
} }
} }
default:
return state
} }
} }
@@ -354,7 +389,7 @@ export function createComposerState({
initMention: string | undefined initMention: string | undefined
initImageUris: ComposerOpts['imageUris'] initImageUris: ComposerOpts['imageUris']
initQuoteUri: string | undefined initQuoteUri: string | undefined
}): ComposerDraft { }): ComposerState {
let media: ImagesMedia | undefined let media: ImagesMedia | undefined
if (initImageUris?.length) { if (initImageUris?.length) {
media = { media = {
@@ -385,14 +420,21 @@ export function createComposerState({
: '', : '',
}) })
return { return {
activePostIndex: 0,
thread: {
posts: [
{
richtext: initRichText, richtext: initRichText,
labels: [], labels: [],
postgate: createPostgateRecord({post: ''}),
threadgate: threadgateViewToAllowUISetting(undefined),
embed: { embed: {
quote, quote,
media, media,
link: undefined, link: undefined,
}, },
},
],
postgate: createPostgateRecord({post: ''}),
threadgate: threadgateViewToAllowUISetting(undefined),
},
} }
} }