Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

[Video] Error handling in composer, fix auto-send (#5122)

* tweak

* error state for upload toolbar

* catch errors in upload status query

* stop query on error

---------

Co-authored-by: Hailey <me@haileyok.com>

authored by

Samuel Newman
Hailey
and committed by
GitHub
3ee5ef32 0bd0146e

+204 -139
+29 -10
src/state/queries/video/video.ts
··· 1 - import React, {useCallback} from 'react' 1 + import React, {useCallback, useEffect} from 'react' 2 2 import {ImagePickerAsset} from 'expo-image-picker' 3 3 import {AppBskyVideoDefs, BlobRef} from '@atproto/api' 4 4 import {msg} from '@lingui/macro' ··· 25 25 | {type: 'SetDimensions'; width: number; height: number} 26 26 | {type: 'SetVideo'; video: CompressedVideo} 27 27 | {type: 'SetJobStatus'; jobStatus: AppBskyVideoDefs.JobStatus} 28 - | {type: 'SetBlobRef'; blobRef: BlobRef} 28 + | {type: 'SetComplete'; blobRef: BlobRef} 29 29 30 30 export interface State { 31 31 status: Status ··· 36 36 blobRef?: BlobRef 37 37 error?: string 38 38 abortController: AbortController 39 + pendingPublish?: {blobRef: BlobRef; mutableProcessed: boolean} 39 40 } 40 41 41 42 function reducer(queryClient: QueryClient) { ··· 77 78 updatedState = {...state, video: action.video, status: 'uploading'} 78 79 } else if (action.type === 'SetJobStatus') { 79 80 updatedState = {...state, jobStatus: action.jobStatus} 80 - } else if (action.type === 'SetBlobRef') { 81 - updatedState = {...state, blobRef: action.blobRef, status: 'done'} 81 + } else if (action.type === 'SetComplete') { 82 + updatedState = { 83 + ...state, 84 + pendingPublish: { 85 + blobRef: action.blobRef, 86 + mutableProcessed: false, 87 + }, 88 + status: 'done', 89 + } 82 90 } 83 91 return updatedState 84 92 } ··· 86 94 87 95 export function useUploadVideo({ 88 96 setStatus, 89 - onSuccess, 90 97 }: { 91 98 setStatus: (status: string) => void 92 99 onSuccess: () => void ··· 112 119 }, 113 120 onSuccess: blobRef => { 114 121 dispatch({ 115 - type: 'SetBlobRef', 122 + type: 'SetComplete', 116 123 blobRef, 117 124 }) 118 - onSuccess() 119 125 }, 126 + onError: useCallback(() => { 127 + dispatch({ 128 + type: 'SetError', 129 + error: _(msg`Video failed to process`), 130 + }) 131 + }, [_]), 120 132 }) 121 133 122 134 const {mutate: onVideoCompressed} = useUploadVideoMutation({ ··· 215 227 const useUploadStatusQuery = ({ 216 228 onStatusChange, 217 229 onSuccess, 230 + onError, 218 231 }: { 219 232 onStatusChange: (status: AppBskyVideoDefs.JobStatus) => void 220 233 onSuccess: (blobRef: BlobRef) => void 234 + onError: (error: Error) => void 221 235 }) => { 222 236 const videoAgent = useVideoAgent() 223 237 const [enabled, setEnabled] = React.useState(true) 224 238 const [jobId, setJobId] = React.useState<string>() 225 239 226 - const {isLoading, isError} = useQuery({ 240 + const {error} = useQuery({ 227 241 queryKey: ['video', 'upload status', jobId], 228 242 queryFn: async () => { 229 243 if (!jobId) return // this won't happen, can ignore ··· 245 259 refetchInterval: 1500, 246 260 }) 247 261 262 + useEffect(() => { 263 + if (error) { 264 + onError(error) 265 + setEnabled(false) 266 + } 267 + }, [error, onError]) 268 + 248 269 return { 249 - isLoading, 250 - isError, 251 270 setJobId: (_jobId: string) => { 252 271 setJobId(_jobId) 253 272 setEnabled(true)
+175 -129
src/view/com/composer/Composer.tsx
··· 190 190 } 191 191 }, 192 192 }) 193 + 193 194 const [publishOnUpload, setPublishOnUpload] = useState(false) 194 195 195 196 const {extLink, setExtLink} = useExternalLinkFetch({setQuote, setError}) ··· 303 304 return false 304 305 }, [gallery.needsAltText, extLink, extGif, requireAltTextEnabled]) 305 306 306 - const onPressPublish = async (finishedUploading?: boolean) => { 307 - if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) { 308 - return 309 - } 307 + const onPressPublish = React.useCallback( 308 + async (finishedUploading?: boolean) => { 309 + if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) { 310 + return 311 + } 310 312 311 - if (isAltTextRequiredAndMissing) { 312 - return 313 - } 313 + if (isAltTextRequiredAndMissing) { 314 + return 315 + } 314 316 315 - if ( 316 - !finishedUploading && 317 - videoUploadState.asset && 318 - videoUploadState.status !== 'done' 319 - ) { 320 - setPublishOnUpload(true) 321 - return 322 - } 317 + if ( 318 + !finishedUploading && 319 + videoUploadState.asset && 320 + videoUploadState.status !== 'done' 321 + ) { 322 + setPublishOnUpload(true) 323 + return 324 + } 323 325 324 - setError('') 326 + setError('') 325 327 326 - if ( 327 - richtext.text.trim().length === 0 && 328 - gallery.isEmpty && 329 - !extLink && 330 - !quote 331 - ) { 332 - setError(_(msg`Did you want to say anything?`)) 333 - return 334 - } 335 - if (extLink?.isLoading) { 336 - setError(_(msg`Please wait for your link card to finish loading`)) 337 - return 338 - } 328 + if ( 329 + richtext.text.trim().length === 0 && 330 + gallery.isEmpty && 331 + !extLink && 332 + !quote 333 + ) { 334 + setError(_(msg`Did you want to say anything?`)) 335 + return 336 + } 337 + if (extLink?.isLoading) { 338 + setError(_(msg`Please wait for your link card to finish loading`)) 339 + return 340 + } 339 341 340 - setIsProcessing(true) 342 + setIsProcessing(true) 341 343 342 - let postUri 343 - try { 344 - postUri = ( 345 - await apilib.post(agent, { 346 - rawText: richtext.text, 347 - replyTo: replyTo?.uri, 348 - images: gallery.images, 349 - quote, 350 - extLink, 351 - labels, 352 - threadgate: threadgateAllowUISettings, 353 - postgate, 354 - onStateChange: setProcessingState, 355 - langs: toPostLanguages(langPrefs.postLanguage), 356 - video: videoUploadState.blobRef 357 - ? { 358 - blobRef: videoUploadState.blobRef, 359 - altText: videoAltText, 360 - captions: captions, 361 - aspectRatio: videoUploadState.asset 362 - ? { 363 - width: videoUploadState.asset?.width, 364 - height: videoUploadState.asset?.height, 365 - } 366 - : undefined, 367 - } 368 - : undefined, 369 - }) 370 - ).uri 344 + let postUri 371 345 try { 372 - await whenAppViewReady(agent, postUri, res => { 373 - const thread = res.data.thread 374 - return AppBskyFeedDefs.isThreadViewPost(thread) 346 + postUri = ( 347 + await apilib.post(agent, { 348 + rawText: richtext.text, 349 + replyTo: replyTo?.uri, 350 + images: gallery.images, 351 + quote, 352 + extLink, 353 + labels, 354 + threadgate: threadgateAllowUISettings, 355 + postgate, 356 + onStateChange: setProcessingState, 357 + langs: toPostLanguages(langPrefs.postLanguage), 358 + video: videoUploadState.pendingPublish?.blobRef 359 + ? { 360 + blobRef: videoUploadState.pendingPublish.blobRef, 361 + altText: videoAltText, 362 + captions: captions, 363 + aspectRatio: videoUploadState.asset 364 + ? { 365 + width: videoUploadState.asset?.width, 366 + height: videoUploadState.asset?.height, 367 + } 368 + : undefined, 369 + } 370 + : undefined, 371 + }) 372 + ).uri 373 + try { 374 + await whenAppViewReady(agent, postUri, res => { 375 + const thread = res.data.thread 376 + return AppBskyFeedDefs.isThreadViewPost(thread) 377 + }) 378 + } catch (waitErr: any) { 379 + logger.error(waitErr, { 380 + message: `Waiting for app view failed`, 381 + }) 382 + // Keep going because the post *was* published. 383 + } 384 + } catch (e: any) { 385 + logger.error(e, { 386 + message: `Composer: create post failed`, 387 + hasImages: gallery.size > 0, 375 388 }) 376 - } catch (waitErr: any) { 377 - logger.error(waitErr, { 378 - message: `Waiting for app view failed`, 389 + 390 + if (extLink) { 391 + setExtLink({ 392 + ...extLink, 393 + isLoading: true, 394 + localThumb: undefined, 395 + } as apilib.ExternalEmbedDraft) 396 + } 397 + let err = cleanError(e.message) 398 + if (err.includes('not locate record')) { 399 + err = _( 400 + msg`We're sorry! The post you are replying to has been deleted.`, 401 + ) 402 + } 403 + setError(err) 404 + setIsProcessing(false) 405 + return 406 + } finally { 407 + if (postUri) { 408 + logEvent('post:create', { 409 + imageCount: gallery.size, 410 + isReply: replyTo != null, 411 + hasLink: extLink != null, 412 + hasQuote: quote != null, 413 + langs: langPrefs.postLanguage, 414 + logContext: 'Composer', 415 + }) 416 + } 417 + track('Create Post', { 418 + imageCount: gallery.size, 379 419 }) 380 - // Keep going because the post *was* published. 420 + if (replyTo && replyTo.uri) track('Post:Reply') 381 421 } 382 - } catch (e: any) { 383 - logger.error(e, { 384 - message: `Composer: create post failed`, 385 - hasImages: gallery.size > 0, 386 - }) 387 - 388 - if (extLink) { 389 - setExtLink({ 390 - ...extLink, 391 - isLoading: true, 392 - localThumb: undefined, 393 - } as apilib.ExternalEmbedDraft) 422 + if (postUri && !replyTo) { 423 + emitPostCreated() 394 424 } 395 - let err = cleanError(e.message) 396 - if (err.includes('not locate record')) { 397 - err = _( 398 - msg`We're sorry! The post you are replying to has been deleted.`, 399 - ) 425 + setLangPrefs.savePostLanguageToHistory() 426 + if (quote) { 427 + // We want to wait for the quote count to update before we call `onPost`, which will refetch data 428 + whenAppViewReady(agent, quote.uri, res => { 429 + const thread = res.data.thread 430 + if ( 431 + AppBskyFeedDefs.isThreadViewPost(thread) && 432 + thread.post.quoteCount !== quoteCount 433 + ) { 434 + onPost?.(postUri) 435 + return true 436 + } 437 + return false 438 + }) 439 + } else { 440 + onPost?.(postUri) 400 441 } 401 - setError(err) 402 - setIsProcessing(false) 403 - return 404 - } finally { 405 - if (postUri) { 406 - logEvent('post:create', { 407 - imageCount: gallery.size, 408 - isReply: replyTo != null, 409 - hasLink: extLink != null, 410 - hasQuote: quote != null, 411 - langs: langPrefs.postLanguage, 412 - logContext: 'Composer', 413 - }) 442 + onClose() 443 + Toast.show( 444 + replyTo 445 + ? _(msg`Your reply has been published`) 446 + : _(msg`Your post has been published`), 447 + ) 448 + }, 449 + [ 450 + _, 451 + agent, 452 + captions, 453 + extLink, 454 + gallery.images, 455 + gallery.isEmpty, 456 + gallery.size, 457 + graphemeLength, 458 + isAltTextRequiredAndMissing, 459 + isProcessing, 460 + labels, 461 + langPrefs.postLanguage, 462 + onClose, 463 + onPost, 464 + postgate, 465 + quote, 466 + quoteCount, 467 + replyTo, 468 + richtext.text, 469 + setExtLink, 470 + setLangPrefs, 471 + threadgateAllowUISettings, 472 + track, 473 + videoAltText, 474 + videoUploadState.asset, 475 + videoUploadState.pendingPublish, 476 + videoUploadState.status, 477 + ], 478 + ) 479 + 480 + React.useEffect(() => { 481 + if (videoUploadState.pendingPublish && publishOnUpload) { 482 + if (!videoUploadState.pendingPublish.mutableProcessed) { 483 + videoUploadState.pendingPublish.mutableProcessed = true 484 + onPressPublish(true) 414 485 } 415 - track('Create Post', { 416 - imageCount: gallery.size, 417 - }) 418 - if (replyTo && replyTo.uri) track('Post:Reply') 419 486 } 420 - if (postUri && !replyTo) { 421 - emitPostCreated() 422 - } 423 - setLangPrefs.savePostLanguageToHistory() 424 - if (quote) { 425 - // We want to wait for the quote count to update before we call `onPost`, which will refetch data 426 - whenAppViewReady(agent, quote.uri, res => { 427 - const thread = res.data.thread 428 - if ( 429 - AppBskyFeedDefs.isThreadViewPost(thread) && 430 - thread.post.quoteCount !== quoteCount 431 - ) { 432 - onPost?.(postUri) 433 - return true 434 - } 435 - return false 436 - }) 437 - } else { 438 - onPost?.(postUri) 439 - } 440 - onClose() 441 - Toast.show( 442 - replyTo 443 - ? _(msg`Your reply has been published`) 444 - : _(msg`Your post has been published`), 445 - ) 446 - } 487 + }, [onPressPublish, publishOnUpload, videoUploadState.pendingPublish]) 447 488 448 489 const canPost = useMemo( 449 490 () => graphemeLength <= MAX_GRAPHEME_LENGTH && !isAltTextRequiredAndMissing, ··· 1058 1099 } 1059 1100 1060 1101 // we could use state.jobStatus?.progress but 99% of the time it jumps from 0 to 100 1061 - const progress = 1102 + let progress = 1062 1103 state.status === 'compressing' || state.status === 'uploading' 1063 1104 ? state.progress 1064 1105 : 100 1065 1106 1107 + if (state.error) { 1108 + text = _('Error') 1109 + progress = 100 1110 + } 1111 + 1066 1112 return ( 1067 1113 <ToolbarWrapper style={[a.flex_row, a.align_center, {paddingVertical: 5}]}> 1068 1114 <ProgressCircle 1069 1115 size={30} 1070 1116 borderWidth={1} 1071 1117 borderColor={t.atoms.border_contrast_low.borderColor} 1072 - color={t.palette.primary_500} 1118 + color={state.error ? t.palette.negative_500 : t.palette.primary_500} 1073 1119 progress={progress} 1074 1120 /> 1075 1121 <NewText style={[a.font_bold, a.ml_sm]}>{text}</NewText>