From 3ee5ef32d9d4342c3ce473933d84aa2ef01dd97b Mon Sep 17 00:00:00 2001
From: Samuel Newman <mozzius@protonmail.com>
Date: Tue, 3 Sep 2024 22:49:19 +0100
Subject: [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>
---
 src/view/com/composer/Composer.tsx | 306 +++++++++++++++++++++----------------
 1 file changed, 176 insertions(+), 130 deletions(-)

(limited to 'src/view/com/composer/Composer.tsx')

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>
-- 
cgit 1.4.1

