Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Make composer reducer source of truth for images/video when publishing (#5595)

* Move caption and altText state into video reducer

* Make composer state source of truth for images and video publish

authored by

dan and committed by
GitHub
09caf327 282db85c

+89 -50
+15 -17
src/lib/api/index.ts
··· 1 1 import { 2 - AppBskyEmbedDefs, 3 2 AppBskyEmbedExternal, 4 3 AppBskyEmbedImages, 5 4 AppBskyEmbedRecord, ··· 7 6 AppBskyEmbedVideo, 8 7 AppBskyFeedPostgate, 9 8 AtUri, 10 - BlobRef, 11 9 BskyAgent, 12 10 ComAtprotoLabelDefs, 13 11 RichText, ··· 46 44 uri: string 47 45 cid: string 48 46 } 49 - video?: { 50 - blobRef: BlobRef 51 - altText: string 52 - captions: {lang: string; file: File}[] 53 - aspectRatio?: AppBskyEmbedDefs.AspectRatio 54 - } 55 47 extLink?: ExternalEmbedDraft 56 - images?: ComposerImage[] 57 48 labels?: string[] 58 49 threadgate: ThreadgateAllowUISetting[] 59 50 postgate: AppBskyFeedPostgate.Record ··· 230 221 | AppBskyEmbedVideo.Main 231 222 | undefined 232 223 > { 233 - if (opts.images?.length) { 224 + const state = opts.composerState 225 + const media = state.embed.media 226 + if (media?.type === 'images') { 234 227 logger.debug(`Uploading images`, { 235 - count: opts.images.length, 228 + count: media.images.length, 236 229 }) 237 230 opts.onStateChange?.(`Uploading images...`) 238 231 const images: AppBskyEmbedImages.Image[] = await Promise.all( 239 - opts.images.map(async (image, i) => { 232 + media.images.map(async (image, i) => { 240 233 logger.debug(`Compressing image #${i}`) 241 234 const {path, width, height, mime} = await compressImage(image) 242 235 logger.debug(`Uploading image #${i}`) ··· 253 246 images, 254 247 } 255 248 } 256 - if (opts.video) { 249 + if (media?.type === 'video' && media.video.status === 'done') { 250 + const video = media.video 257 251 const captions = await Promise.all( 258 - opts.video.captions 252 + video.captions 259 253 .filter(caption => caption.lang !== '') 260 254 .map(async caption => { 261 255 const {data} = await agent.uploadBlob(caption.file, { ··· 266 260 ) 267 261 return { 268 262 $type: 'app.bsky.embed.video', 269 - video: opts.video.blobRef, 270 - alt: opts.video.altText || undefined, 263 + video: video.pendingPublish.blobRef, 264 + alt: video.altText || undefined, 271 265 captions: captions.length === 0 ? undefined : captions, 272 - aspectRatio: opts.video.aspectRatio, 266 + aspectRatio: { 267 + width: video.asset.width, 268 + height: video.asset.height, 269 + }, 273 270 } 274 271 } 275 272 if (opts.extLink) { 273 + // TODO: Read this from composer state as well. 276 274 if (opts.extLink.embed) { 277 275 return undefined 278 276 }
+23 -24
src/view/com/composer/Composer.tsx
··· 184 184 initQuote, 185 185 ) 186 186 187 - const [videoAltText, setVideoAltText] = useState('') 188 - const [captions, setCaptions] = useState<{lang: string; file: File}[]>([]) 189 - 190 187 // TODO: Move more state here. 191 188 const [composerState, dispatch] = useReducer( 192 189 composerReducer, ··· 422 419 try { 423 420 postUri = ( 424 421 await apilib.post(agent, { 425 - composerState, // TODO: not used yet. 422 + composerState, // TODO: move more state here. 426 423 rawText: richtext.text, 427 424 replyTo: replyTo?.uri, 428 - images, 429 425 quote, 430 426 extLink, 431 427 labels, ··· 433 429 postgate, 434 430 onStateChange: setProcessingState, 435 431 langs: toPostLanguages(langPrefs.postLanguage), 436 - video: 437 - videoState.status === 'done' 438 - ? { 439 - blobRef: videoState.pendingPublish.blobRef, 440 - altText: videoAltText, 441 - captions: captions, 442 - aspectRatio: { 443 - width: videoState.asset.width, 444 - height: videoState.asset.height, 445 - }, 446 - } 447 - : undefined, 448 432 }) 449 433 ).uri 450 434 try { ··· 522 506 [ 523 507 _, 524 508 agent, 525 - captions, 526 509 composerState, 527 510 extLink, 528 511 images, ··· 541 524 setExtLink, 542 525 setLangPrefs, 543 526 threadgateAllowUISettings, 544 - videoAltText, 545 527 videoState.asset, 546 - videoState.pendingPublish, 547 528 videoState.status, 548 529 ], 549 530 ) ··· 811 792 /> 812 793 ) : null)} 813 794 <SubtitleDialogBtn 814 - defaultAltText={videoAltText} 815 - saveAltText={setVideoAltText} 816 - captions={captions} 817 - setCaptions={setCaptions} 795 + defaultAltText={videoState.altText} 796 + saveAltText={altText => 797 + dispatch({ 798 + type: 'embed_update_video', 799 + videoAction: { 800 + type: 'update_alt_text', 801 + altText, 802 + signal: videoState.abortController.signal, 803 + }, 804 + }) 805 + } 806 + captions={videoState.captions} 807 + setCaptions={updater => { 808 + dispatch({ 809 + type: 'embed_update_video', 810 + videoAction: { 811 + type: 'update_captions', 812 + updater, 813 + signal: videoState.abortController.signal, 814 + }, 815 + }) 816 + }} 818 817 /> 819 818 </Animated.View> 820 819 )}
+46 -2
src/view/com/composer/state/video.ts
··· 4 4 import {I18n} from '@lingui/core' 5 5 import {msg} from '@lingui/macro' 6 6 7 - import {createVideoAgent} from '#/lib/media/video/util' 8 - import {uploadVideo} from '#/lib/media/video/upload' 9 7 import {AbortError} from '#/lib/async/cancelable' 10 8 import {compressVideo} from '#/lib/media/video/compress' 11 9 import { ··· 14 12 VideoTooLargeError, 15 13 } from '#/lib/media/video/errors' 16 14 import {CompressedVideo} from '#/lib/media/video/types' 15 + import {uploadVideo} from '#/lib/media/video/upload' 16 + import {createVideoAgent} from '#/lib/media/video/util' 17 17 import {logger} from '#/logger' 18 + 19 + type CaptionsTrack = {lang: string; file: File} 18 20 19 21 export type VideoAction = 20 22 | { ··· 41 43 signal: AbortSignal 42 44 } 43 45 | { 46 + type: 'update_alt_text' 47 + altText: string 48 + signal: AbortSignal 49 + } 50 + | { 51 + type: 'update_captions' 52 + updater: (prev: CaptionsTrack[]) => CaptionsTrack[] 53 + signal: AbortSignal 54 + } 55 + | { 44 56 type: 'update_job_status' 45 57 jobStatus: AppBskyVideoDefs.JobStatus 46 58 signal: AbortSignal ··· 57 69 video: undefined, 58 70 jobId: undefined, 59 71 pendingPublish: undefined, 72 + altText: '', 73 + captions: [], 60 74 }) 61 75 62 76 export type NoVideoState = typeof NO_VIDEO ··· 70 84 jobId: string | null 71 85 error: string 72 86 pendingPublish?: undefined 87 + altText: string 88 + captions: CaptionsTrack[] 73 89 } 74 90 75 91 type CompressingState = { ··· 80 96 video?: undefined 81 97 jobId?: undefined 82 98 pendingPublish?: undefined 99 + altText: string 100 + captions: CaptionsTrack[] 83 101 } 84 102 85 103 type UploadingState = { ··· 90 108 video: CompressedVideo 91 109 jobId?: undefined 92 110 pendingPublish?: undefined 111 + altText: string 112 + captions: CaptionsTrack[] 93 113 } 94 114 95 115 type ProcessingState = { ··· 101 121 jobId: string 102 122 jobStatus: AppBskyVideoDefs.JobStatus | null 103 123 pendingPublish?: undefined 124 + altText: string 125 + captions: CaptionsTrack[] 104 126 } 105 127 106 128 type DoneState = { ··· 111 133 video: CompressedVideo 112 134 jobId?: undefined 113 135 pendingPublish: {blobRef: BlobRef; mutableProcessed: boolean} 136 + altText: string 137 + captions: CaptionsTrack[] 114 138 } 115 139 116 140 export type VideoState = ··· 129 153 progress: 0, 130 154 abortController, 131 155 asset, 156 + altText: '', 157 + captions: [], 132 158 } 133 159 } 134 160 ··· 149 175 asset: state.asset ?? null, 150 176 video: state.video ?? null, 151 177 jobId: state.jobId ?? null, 178 + altText: state.altText, 179 + captions: state.captions, 152 180 } 153 181 } else if (action.type === 'update_progress') { 154 182 if (state.status === 'compressing' || state.status === 'uploading') { ··· 164 192 asset: {...state.asset, width: action.width, height: action.height}, 165 193 } 166 194 } 195 + } else if (action.type === 'update_alt_text') { 196 + return { 197 + ...state, 198 + altText: action.altText, 199 + } 200 + } else if (action.type === 'update_captions') { 201 + return { 202 + ...state, 203 + captions: action.updater(state.captions), 204 + } 167 205 } else if (action.type === 'compressing_to_uploading') { 168 206 if (state.status === 'compressing') { 169 207 return { ··· 172 210 abortController: state.abortController, 173 211 asset: state.asset, 174 212 video: action.video, 213 + altText: state.altText, 214 + captions: state.captions, 175 215 } 176 216 } 177 217 return state ··· 185 225 video: state.video, 186 226 jobId: action.jobId, 187 227 jobStatus: null, 228 + altText: state.altText, 229 + captions: state.captions, 188 230 } 189 231 } 190 232 } else if (action.type === 'update_job_status') { ··· 210 252 blobRef: action.blobRef, 211 253 mutableProcessed: false, 212 254 }, 255 + altText: state.altText, 256 + captions: state.captions, 213 257 } 214 258 } 215 259 }
+5 -7
src/view/com/composer/videos/SubtitleDialog.tsx
··· 22 22 23 23 const MAX_NUM_CAPTIONS = 1 24 24 25 + type CaptionsTrack = {lang: string; file: File} 26 + 25 27 interface Props { 26 28 defaultAltText: string 27 - captions: {lang: string; file: File}[] 29 + captions: CaptionsTrack[] 28 30 saveAltText: (altText: string) => void 29 - setCaptions: React.Dispatch< 30 - React.SetStateAction<{lang: string; file: File}[]> 31 - > 31 + setCaptions: (updater: (prev: CaptionsTrack[]) => CaptionsTrack[]) => void 32 32 } 33 33 34 34 export function SubtitleDialogBtn(props: Props) { ··· 198 198 language: string 199 199 file: File 200 200 otherLanguages: {code2: string; code3: string; name: string}[] 201 - setCaptions: React.Dispatch< 202 - React.SetStateAction<{lang: string; file: File}[]> 203 - > 201 + setCaptions: (updater: (prev: CaptionsTrack[]) => CaptionsTrack[]) => void 204 202 style: StyleProp<ViewStyle> 205 203 }) { 206 204 const {_} = useLingui()