about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2024-09-03 22:49:19 +0100
committerGitHub <noreply@github.com>2024-09-03 22:49:19 +0100
commit3ee5ef32d9d4342c3ce473933d84aa2ef01dd97b (patch)
tree092bad388710c9d399e99635c6f60c9ade682905 /src
parent0bd0146efb9e3fe7676826e4e95c3461d1a1da6a (diff)
downloadvoidsky-3ee5ef32d9d4342c3ce473933d84aa2ef01dd97b.tar.zst
[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>
Diffstat (limited to 'src')
-rw-r--r--src/state/queries/video/video.ts39
-rw-r--r--src/view/com/composer/Composer.tsx306
2 files changed, 205 insertions, 140 deletions
diff --git a/src/state/queries/video/video.ts b/src/state/queries/video/video.ts
index 5e36ce358..ee0724498 100644
--- a/src/state/queries/video/video.ts
+++ b/src/state/queries/video/video.ts
@@ -1,4 +1,4 @@
-import React, {useCallback} from 'react'
+import React, {useCallback, useEffect} from 'react'
 import {ImagePickerAsset} from 'expo-image-picker'
 import {AppBskyVideoDefs, BlobRef} from '@atproto/api'
 import {msg} from '@lingui/macro'
@@ -25,7 +25,7 @@ type Action =
   | {type: 'SetDimensions'; width: number; height: number}
   | {type: 'SetVideo'; video: CompressedVideo}
   | {type: 'SetJobStatus'; jobStatus: AppBskyVideoDefs.JobStatus}
-  | {type: 'SetBlobRef'; blobRef: BlobRef}
+  | {type: 'SetComplete'; blobRef: BlobRef}
 
 export interface State {
   status: Status
@@ -36,6 +36,7 @@ export interface State {
   blobRef?: BlobRef
   error?: string
   abortController: AbortController
+  pendingPublish?: {blobRef: BlobRef; mutableProcessed: boolean}
 }
 
 function reducer(queryClient: QueryClient) {
@@ -77,8 +78,15 @@ function reducer(queryClient: QueryClient) {
       updatedState = {...state, video: action.video, status: 'uploading'}
     } else if (action.type === 'SetJobStatus') {
       updatedState = {...state, jobStatus: action.jobStatus}
-    } else if (action.type === 'SetBlobRef') {
-      updatedState = {...state, blobRef: action.blobRef, status: 'done'}
+    } else if (action.type === 'SetComplete') {
+      updatedState = {
+        ...state,
+        pendingPublish: {
+          blobRef: action.blobRef,
+          mutableProcessed: false,
+        },
+        status: 'done',
+      }
     }
     return updatedState
   }
@@ -86,7 +94,6 @@ function reducer(queryClient: QueryClient) {
 
 export function useUploadVideo({
   setStatus,
-  onSuccess,
 }: {
   setStatus: (status: string) => void
   onSuccess: () => void
@@ -112,11 +119,16 @@ export function useUploadVideo({
     },
     onSuccess: blobRef => {
       dispatch({
-        type: 'SetBlobRef',
+        type: 'SetComplete',
         blobRef,
       })
-      onSuccess()
     },
+    onError: useCallback(() => {
+      dispatch({
+        type: 'SetError',
+        error: _(msg`Video failed to process`),
+      })
+    }, [_]),
   })
 
   const {mutate: onVideoCompressed} = useUploadVideoMutation({
@@ -215,15 +227,17 @@ export function useUploadVideo({
 const useUploadStatusQuery = ({
   onStatusChange,
   onSuccess,
+  onError,
 }: {
   onStatusChange: (status: AppBskyVideoDefs.JobStatus) => void
   onSuccess: (blobRef: BlobRef) => void
+  onError: (error: Error) => void
 }) => {
   const videoAgent = useVideoAgent()
   const [enabled, setEnabled] = React.useState(true)
   const [jobId, setJobId] = React.useState<string>()
 
-  const {isLoading, isError} = useQuery({
+  const {error} = useQuery({
     queryKey: ['video', 'upload status', jobId],
     queryFn: async () => {
       if (!jobId) return // this won't happen, can ignore
@@ -245,9 +259,14 @@ const useUploadStatusQuery = ({
     refetchInterval: 1500,
   })
 
+  useEffect(() => {
+    if (error) {
+      onError(error)
+      setEnabled(false)
+    }
+  }, [error, onError])
+
   return {
-    isLoading,
-    isError,
     setJobId: (_jobId: string) => {
       setJobId(_jobId)
       setEnabled(true)
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index e42e23ba5..b07adf2ad 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -190,6 +190,7 @@ export const ComposePost = observer(function ComposePost({
       }
     },
   })
+
   const [publishOnUpload, setPublishOnUpload] = useState(false)
 
   const {extLink, setExtLink} = useExternalLinkFetch({setQuote, setError})
@@ -303,147 +304,187 @@ export const ComposePost = observer(function ComposePost({
     return false
   }, [gallery.needsAltText, extLink, extGif, requireAltTextEnabled])
 
-  const onPressPublish = async (finishedUploading?: boolean) => {
-    if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) {
-      return
-    }
+  const onPressPublish = React.useCallback(
+    async (finishedUploading?: boolean) => {
+      if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) {
+        return
+      }
 
-    if (isAltTextRequiredAndMissing) {
-      return
-    }
+      if (isAltTextRequiredAndMissing) {
+        return
+      }
 
-    if (
-      !finishedUploading &&
-      videoUploadState.asset &&
-      videoUploadState.status !== 'done'
-    ) {
-      setPublishOnUpload(true)
-      return
-    }
+      if (
+        !finishedUploading &&
+        videoUploadState.asset &&
+        videoUploadState.status !== 'done'
+      ) {
+        setPublishOnUpload(true)
+        return
+      }
 
-    setError('')
+      setError('')
 
-    if (
-      richtext.text.trim().length === 0 &&
-      gallery.isEmpty &&
-      !extLink &&
-      !quote
-    ) {
-      setError(_(msg`Did you want to say anything?`))
-      return
-    }
-    if (extLink?.isLoading) {
-      setError(_(msg`Please wait for your link card to finish loading`))
-      return
-    }
+      if (
+        richtext.text.trim().length === 0 &&
+        gallery.isEmpty &&
+        !extLink &&
+        !quote
+      ) {
+        setError(_(msg`Did you want to say anything?`))
+        return
+      }
+      if (extLink?.isLoading) {
+        setError(_(msg`Please wait for your link card to finish loading`))
+        return
+      }
 
-    setIsProcessing(true)
-
-    let postUri
-    try {
-      postUri = (
-        await apilib.post(agent, {
-          rawText: richtext.text,
-          replyTo: replyTo?.uri,
-          images: gallery.images,
-          quote,
-          extLink,
-          labels,
-          threadgate: threadgateAllowUISettings,
-          postgate,
-          onStateChange: setProcessingState,
-          langs: toPostLanguages(langPrefs.postLanguage),
-          video: videoUploadState.blobRef
-            ? {
-                blobRef: videoUploadState.blobRef,
-                altText: videoAltText,
-                captions: captions,
-                aspectRatio: videoUploadState.asset
-                  ? {
-                      width: videoUploadState.asset?.width,
-                      height: videoUploadState.asset?.height,
-                    }
-                  : undefined,
-              }
-            : undefined,
-        })
-      ).uri
+      setIsProcessing(true)
+
+      let postUri
       try {
-        await whenAppViewReady(agent, postUri, res => {
-          const thread = res.data.thread
-          return AppBskyFeedDefs.isThreadViewPost(thread)
-        })
-      } catch (waitErr: any) {
-        logger.error(waitErr, {
-          message: `Waiting for app view failed`,
+        postUri = (
+          await apilib.post(agent, {
+            rawText: richtext.text,
+            replyTo: replyTo?.uri,
+            images: gallery.images,
+            quote,
+            extLink,
+            labels,
+            threadgate: threadgateAllowUISettings,
+            postgate,
+            onStateChange: setProcessingState,
+            langs: toPostLanguages(langPrefs.postLanguage),
+            video: videoUploadState.pendingPublish?.blobRef
+              ? {
+                  blobRef: videoUploadState.pendingPublish.blobRef,
+                  altText: videoAltText,
+                  captions: captions,
+                  aspectRatio: videoUploadState.asset
+                    ? {
+                        width: videoUploadState.asset?.width,
+                        height: videoUploadState.asset?.height,
+                      }
+                    : undefined,
+                }
+              : undefined,
+          })
+        ).uri
+        try {
+          await whenAppViewReady(agent, postUri, res => {
+            const thread = res.data.thread
+            return AppBskyFeedDefs.isThreadViewPost(thread)
+          })
+        } catch (waitErr: any) {
+          logger.error(waitErr, {
+            message: `Waiting for app view failed`,
+          })
+          // Keep going because the post *was* published.
+        }
+      } catch (e: any) {
+        logger.error(e, {
+          message: `Composer: create post failed`,
+          hasImages: gallery.size > 0,
         })
-        // Keep going because the post *was* published.
-      }
-    } catch (e: any) {
-      logger.error(e, {
-        message: `Composer: create post failed`,
-        hasImages: gallery.size > 0,
-      })
 
-      if (extLink) {
-        setExtLink({
-          ...extLink,
-          isLoading: true,
-          localThumb: undefined,
-        } as apilib.ExternalEmbedDraft)
+        if (extLink) {
+          setExtLink({
+            ...extLink,
+            isLoading: true,
+            localThumb: undefined,
+          } as apilib.ExternalEmbedDraft)
+        }
+        let err = cleanError(e.message)
+        if (err.includes('not locate record')) {
+          err = _(
+            msg`We're sorry! The post you are replying to has been deleted.`,
+          )
+        }
+        setError(err)
+        setIsProcessing(false)
+        return
+      } finally {
+        if (postUri) {
+          logEvent('post:create', {
+            imageCount: gallery.size,
+            isReply: replyTo != null,
+            hasLink: extLink != null,
+            hasQuote: quote != null,
+            langs: langPrefs.postLanguage,
+            logContext: 'Composer',
+          })
+        }
+        track('Create Post', {
+          imageCount: gallery.size,
+        })
+        if (replyTo && replyTo.uri) track('Post:Reply')
       }
-      let err = cleanError(e.message)
-      if (err.includes('not locate record')) {
-        err = _(
-          msg`We're sorry! The post you are replying to has been deleted.`,
-        )
+      if (postUri && !replyTo) {
+        emitPostCreated()
       }
-      setError(err)
-      setIsProcessing(false)
-      return
-    } finally {
-      if (postUri) {
-        logEvent('post:create', {
-          imageCount: gallery.size,
-          isReply: replyTo != null,
-          hasLink: extLink != null,
-          hasQuote: quote != null,
-          langs: langPrefs.postLanguage,
-          logContext: 'Composer',
+      setLangPrefs.savePostLanguageToHistory()
+      if (quote) {
+        // We want to wait for the quote count to update before we call `onPost`, which will refetch data
+        whenAppViewReady(agent, quote.uri, res => {
+          const thread = res.data.thread
+          if (
+            AppBskyFeedDefs.isThreadViewPost(thread) &&
+            thread.post.quoteCount !== quoteCount
+          ) {
+            onPost?.(postUri)
+            return true
+          }
+          return false
         })
+      } else {
+        onPost?.(postUri)
+      }
+      onClose()
+      Toast.show(
+        replyTo
+          ? _(msg`Your reply has been published`)
+          : _(msg`Your post has been published`),
+      )
+    },
+    [
+      _,
+      agent,
+      captions,
+      extLink,
+      gallery.images,
+      gallery.isEmpty,
+      gallery.size,
+      graphemeLength,
+      isAltTextRequiredAndMissing,
+      isProcessing,
+      labels,
+      langPrefs.postLanguage,
+      onClose,
+      onPost,
+      postgate,
+      quote,
+      quoteCount,
+      replyTo,
+      richtext.text,
+      setExtLink,
+      setLangPrefs,
+      threadgateAllowUISettings,
+      track,
+      videoAltText,
+      videoUploadState.asset,
+      videoUploadState.pendingPublish,
+      videoUploadState.status,
+    ],
+  )
+
+  React.useEffect(() => {
+    if (videoUploadState.pendingPublish && publishOnUpload) {
+      if (!videoUploadState.pendingPublish.mutableProcessed) {
+        videoUploadState.pendingPublish.mutableProcessed = true
+        onPressPublish(true)
       }
-      track('Create Post', {
-        imageCount: gallery.size,
-      })
-      if (replyTo && replyTo.uri) track('Post:Reply')
-    }
-    if (postUri && !replyTo) {
-      emitPostCreated()
-    }
-    setLangPrefs.savePostLanguageToHistory()
-    if (quote) {
-      // We want to wait for the quote count to update before we call `onPost`, which will refetch data
-      whenAppViewReady(agent, quote.uri, res => {
-        const thread = res.data.thread
-        if (
-          AppBskyFeedDefs.isThreadViewPost(thread) &&
-          thread.post.quoteCount !== quoteCount
-        ) {
-          onPost?.(postUri)
-          return true
-        }
-        return false
-      })
-    } else {
-      onPost?.(postUri)
     }
-    onClose()
-    Toast.show(
-      replyTo
-        ? _(msg`Your reply has been published`)
-        : _(msg`Your post has been published`),
-    )
-  }
+  }, [onPressPublish, publishOnUpload, videoUploadState.pendingPublish])
 
   const canPost = useMemo(
     () => graphemeLength <= MAX_GRAPHEME_LENGTH && !isAltTextRequiredAndMissing,
@@ -1058,18 +1099,23 @@ function VideoUploadToolbar({state}: {state: VideoUploadState}) {
   }
 
   // we could use state.jobStatus?.progress but 99% of the time it jumps from 0 to 100
-  const progress =
+  let progress =
     state.status === 'compressing' || state.status === 'uploading'
       ? state.progress
       : 100
 
+  if (state.error) {
+    text = _('Error')
+    progress = 100
+  }
+
   return (
     <ToolbarWrapper style={[a.flex_row, a.align_center, {paddingVertical: 5}]}>
       <ProgressCircle
         size={30}
         borderWidth={1}
         borderColor={t.atoms.border_contrast_low.borderColor}
-        color={t.palette.primary_500}
+        color={state.error ? t.palette.negative_500 : t.palette.primary_500}
         progress={progress}
       />
       <NewText style={[a.font_bold, a.ml_sm]}>{text}</NewText>