Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor composer state for threads (#5945)

* Refactor composer state for threads

* Remove unnecessary default case

TS can see it's exhaustive.

authored by

dan and committed by
GitHub
bab44a5a 3bf91eb8

+126 -58
+14 -9
src/lib/api/index.ts
··· 30 30 createThreadgateRecord, 31 31 threadgateAllowUISettingToAllowRecordValue, 32 32 } from '#/state/queries/threadgate' 33 - import {ComposerDraft, EmbedDraft} from '#/view/com/composer/state/composer' 33 + import { 34 + EmbedDraft, 35 + PostDraft, 36 + ThreadDraft, 37 + } from '#/view/com/composer/state/composer' 34 38 import {createGIFDescription} from '../gif-alt-text' 35 39 import {uploadBlob} from './upload-blob' 36 40 37 41 export {uploadBlob} 38 42 39 43 interface PostOpts { 40 - draft: ComposerDraft 44 + thread: ThreadDraft 41 45 replyTo?: string 42 46 onStateChange?: (state: string) => void 43 47 langs?: string[] ··· 48 52 queryClient: QueryClient, 49 53 opts: PostOpts, 50 54 ) { 51 - const draft = opts.draft 55 + const thread = opts.thread 56 + const draft = thread.posts[0] // TODO: Support threads. 52 57 53 58 opts.onStateChange?.(t`Processing...`) 54 59 // NB -- Do not await anything here to avoid waterfalls! ··· 111 116 } 112 117 113 118 // Create threadgate record 114 - if (draft.threadgate.some(tg => tg.type !== 'everybody')) { 119 + if (thread.threadgate.some(tg => tg.type !== 'everybody')) { 115 120 const record = createThreadgateRecord({ 116 121 createdAt: date, 117 122 post: uri, 118 - allow: threadgateAllowUISettingToAllowRecordValue(draft.threadgate), 123 + allow: threadgateAllowUISettingToAllowRecordValue(thread.threadgate), 119 124 }) 120 125 121 126 writes.push({ ··· 128 133 129 134 // Create postgate record 130 135 if ( 131 - draft.postgate.embeddingRules?.length || 132 - draft.postgate.detachedEmbeddingUris?.length 136 + thread.postgate.embeddingRules?.length || 137 + thread.postgate.detachedEmbeddingUris?.length 133 138 ) { 134 139 const record: AppBskyFeedPostgate.Record = { 135 - ...draft.postgate, 140 + ...thread.postgate, 136 141 $type: 'app.bsky.feed.postgate', 137 142 createdAt: date, 138 143 post: uri, ··· 198 203 async function resolveEmbed( 199 204 agent: BskyAgent, 200 205 queryClient: QueryClient, 201 - draft: ComposerDraft, 206 + draft: PostDraft, 202 207 onStateChange: ((state: string) => void) | undefined, 203 208 ): Promise< 204 209 | AppBskyEmbedImages.Main
+41 -20
src/view/com/composer/Composer.tsx
··· 114 114 import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' 115 115 import { 116 116 ComposerAction, 117 - ComposerDraft, 118 117 composerReducer, 119 118 createComposerState, 120 119 EmbedDraft, 121 120 MAX_IMAGES, 121 + PostAction, 122 + PostDraft, 123 + ThreadDraft, 122 124 } from './state/composer' 123 125 import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video' 124 126 ··· 161 163 const [publishingStage, setPublishingStage] = useState('') 162 164 const [error, setError] = useState('') 163 165 164 - const [draft, dispatch] = useReducer( 166 + const [composerState, composerDispatch] = useReducer( 165 167 composerReducer, 166 168 {initImageUris, initQuoteUri: initQuote?.uri, initText, initMention}, 167 169 createComposerState, 168 170 ) 171 + 172 + // TODO: Display drafts for other posts in the thread. 173 + const draft = composerState.thread.posts[composerState.activePostIndex] 174 + const dispatch = useCallback((postAction: PostAction) => { 175 + composerDispatch({ 176 + type: 'update_post', 177 + postAction, 178 + }) 179 + }, []) 180 + 169 181 const richtext = draft.richtext 170 182 let quote: string | undefined 171 183 if (draft.embed.quote) { ··· 207 219 _, 208 220 ) 209 221 }, 210 - [_, agent, currentDid], 222 + [_, agent, currentDid, dispatch], 211 223 ) 212 224 213 225 // Whenever we receive an initial video uri, we should immediately run compression if necessary ··· 333 345 try { 334 346 postUri = ( 335 347 await apilib.post(agent, queryClient, { 336 - draft: draft, 348 + thread: composerState.thread, 337 349 replyTo: replyTo?.uri, 338 350 onStateChange: setPublishingStage, 339 351 langs: toPostLanguages(langPrefs.postLanguage), ··· 409 421 [ 410 422 _, 411 423 agent, 412 - draft, 424 + composerState.thread, 413 425 extLink, 414 426 images, 415 427 canPost, ··· 504 516 505 517 <ComposerPills 506 518 isReply={!!replyTo} 507 - draft={draft} 508 - dispatch={dispatch} 519 + post={draft} 520 + thread={composerState.thread} 521 + dispatch={composerDispatch} 509 522 bottomBarAnimatedStyle={bottomBarAnimatedStyle} 510 523 /> 511 524 ··· 543 556 onError, 544 557 onPublish, 545 558 }: { 546 - draft: ComposerDraft 547 - dispatch: (action: ComposerAction) => void 559 + draft: PostDraft 560 + dispatch: (action: PostAction) => void 548 561 textInput: React.Ref<TextInputRef> 549 562 isReply: boolean 550 563 canRemoveQuote: boolean ··· 736 749 canRemoveQuote, 737 750 }: { 738 751 embed: EmbedDraft 739 - dispatch: (action: ComposerAction) => void 752 + dispatch: (action: PostAction) => void 740 753 clearVideo: () => void 741 754 canRemoveQuote: boolean 742 755 }) { ··· 850 863 851 864 function ComposerPills({ 852 865 isReply, 853 - draft, 866 + thread, 867 + post, 854 868 dispatch, 855 869 bottomBarAnimatedStyle, 856 870 }: { 857 871 isReply: boolean 858 - draft: ComposerDraft 872 + thread: ThreadDraft 873 + post: PostDraft 859 874 dispatch: (action: ComposerAction) => void 860 875 bottomBarAnimatedStyle: StyleProp<ViewStyle> 861 876 }) { 862 877 const t = useTheme() 863 - const media = draft.embed.media 878 + const media = post.embed.media 864 879 const hasMedia = media?.type === 'images' || media?.type === 'video' 865 - const hasLink = !!draft.embed.link 880 + const hasLink = !!post.embed.link 866 881 867 882 // Don't render anything if no pills are going to be displayed 868 883 if (isReply && !hasMedia && !hasLink) { ··· 879 894 showsHorizontalScrollIndicator={false}> 880 895 {isReply ? null : ( 881 896 <ThreadgateBtn 882 - postgate={draft.postgate} 897 + postgate={thread.postgate} 883 898 onChangePostgate={nextPostgate => { 884 899 dispatch({type: 'update_postgate', postgate: nextPostgate}) 885 900 }} 886 - threadgateAllowUISettings={draft.threadgate} 901 + threadgateAllowUISettings={thread.threadgate} 887 902 onChangeThreadgateAllowUISettings={nextThreadgate => { 888 903 dispatch({ 889 904 type: 'update_threadgate', ··· 895 910 )} 896 911 {hasMedia || hasLink ? ( 897 912 <LabelsBtn 898 - labels={draft.labels} 913 + labels={post.labels} 899 914 onChange={nextLabels => { 900 - dispatch({type: 'update_labels', labels: nextLabels}) 915 + dispatch({ 916 + type: 'update_post', 917 + postAction: { 918 + type: 'update_labels', 919 + labels: nextLabels, 920 + }, 921 + }) 901 922 }} 902 923 /> 903 924 ) : null} ··· 914 935 onError, 915 936 onSelectVideo, 916 937 }: { 917 - draft: ComposerDraft 918 - dispatch: (action: ComposerAction) => void 938 + draft: PostDraft 939 + dispatch: (action: PostAction) => void 919 940 graphemeLength: number 920 941 onEmojiButtonPress: () => void 921 942 onError: (error: string) => void
+2 -2
src/view/com/composer/photos/Gallery.tsx
··· 21 21 import {Text} from '#/view/com/util/text/Text' 22 22 import {useTheme} from '#/alf' 23 23 import * as Dialog from '#/components/Dialog' 24 - import {ComposerAction} from '../state/composer' 24 + import {PostAction} from '../state/composer' 25 25 import {EditImageDialog} from './EditImageDialog' 26 26 import {ImageAltTextDialog} from './ImageAltTextDialog' 27 27 ··· 29 29 30 30 interface GalleryProps { 31 31 images: ComposerImage[] 32 - dispatch: (action: ComposerAction) => void 32 + dispatch: (action: PostAction) => void 33 33 } 34 34 35 35 export let Gallery = (props: GalleryProps): React.ReactNode => {
+69 -27
src/view/com/composer/state/composer.ts
··· 47 47 link: Link | undefined 48 48 } 49 49 50 - export type ComposerDraft = { 50 + export type PostDraft = { 51 51 richtext: RichText 52 52 labels: SelfLabel[] 53 - postgate: AppBskyFeedPostgate.Record 54 - threadgate: ThreadgateAllowUISetting[] 55 53 embed: EmbedDraft 56 54 } 57 55 58 - export type ComposerAction = 56 + export type PostAction = 59 57 | {type: 'update_richtext'; richtext: RichText} 60 58 | {type: 'update_labels'; labels: SelfLabel[]} 61 - | {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record} 62 - | {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]} 63 59 | {type: 'embed_add_images'; images: ComposerImage[]} 64 60 | {type: 'embed_update_image'; image: ComposerImage} 65 61 | {type: 'embed_remove_image'; image: ComposerImage} ··· 77 73 | {type: 'embed_update_gif'; alt: string} 78 74 | {type: 'embed_remove_gif'} 79 75 76 + export type ThreadDraft = { 77 + posts: PostDraft[] 78 + postgate: AppBskyFeedPostgate.Record 79 + threadgate: ThreadgateAllowUISetting[] 80 + } 81 + 82 + export type ComposerState = { 83 + thread: ThreadDraft 84 + activePostIndex: number // TODO: Add actions to update this. 85 + } 86 + 87 + export type ComposerAction = 88 + | {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record} 89 + | {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]} 90 + | {type: 'update_post'; postAction: PostAction} 91 + 80 92 export const MAX_IMAGES = 4 81 93 82 94 export function composerReducer( 83 - state: ComposerDraft, 95 + state: ComposerState, 84 96 action: ComposerAction, 85 - ): ComposerDraft { 97 + ): ComposerState { 86 98 switch (action.type) { 87 - case 'update_richtext': { 99 + case 'update_postgate': { 100 + return { 101 + ...state, 102 + thread: { 103 + ...state.thread, 104 + postgate: action.postgate, 105 + }, 106 + } 107 + } 108 + case 'update_threadgate': { 88 109 return { 89 110 ...state, 90 - richtext: action.richtext, 111 + thread: { 112 + ...state.thread, 113 + threadgate: action.threadgate, 114 + }, 91 115 } 92 116 } 93 - case 'update_labels': { 117 + case 'update_post': { 118 + const nextPosts = [...state.thread.posts] 119 + nextPosts[state.activePostIndex] = postReducer( 120 + state.thread.posts[state.activePostIndex], 121 + action.postAction, 122 + ) 94 123 return { 95 124 ...state, 96 - labels: action.labels, 125 + thread: { 126 + ...state.thread, 127 + posts: nextPosts, 128 + }, 97 129 } 98 130 } 99 - case 'update_postgate': { 131 + } 132 + } 133 + 134 + function postReducer(state: PostDraft, action: PostAction): PostDraft { 135 + switch (action.type) { 136 + case 'update_richtext': { 100 137 return { 101 138 ...state, 102 - postgate: action.postgate, 139 + richtext: action.richtext, 103 140 } 104 141 } 105 - case 'update_threadgate': { 142 + case 'update_labels': { 106 143 return { 107 144 ...state, 108 - threadgate: action.threadgate, 145 + labels: action.labels, 109 146 } 110 147 } 111 148 case 'embed_add_images': { ··· 339 376 }, 340 377 } 341 378 } 342 - default: 343 - return state 344 379 } 345 380 } 346 381 ··· 354 389 initMention: string | undefined 355 390 initImageUris: ComposerOpts['imageUris'] 356 391 initQuoteUri: string | undefined 357 - }): ComposerDraft { 392 + }): ComposerState { 358 393 let media: ImagesMedia | undefined 359 394 if (initImageUris?.length) { 360 395 media = { ··· 385 420 : '', 386 421 }) 387 422 return { 388 - richtext: initRichText, 389 - labels: [], 390 - postgate: createPostgateRecord({post: ''}), 391 - threadgate: threadgateViewToAllowUISetting(undefined), 392 - embed: { 393 - quote, 394 - media, 395 - link: undefined, 423 + activePostIndex: 0, 424 + thread: { 425 + posts: [ 426 + { 427 + richtext: initRichText, 428 + labels: [], 429 + embed: { 430 + quote, 431 + media, 432 + link: undefined, 433 + }, 434 + }, 435 + ], 436 + postgate: createPostgateRecord({post: ''}), 437 + threadgate: threadgateViewToAllowUISetting(undefined), 396 438 }, 397 439 } 398 440 }