From d2392d2d64c46d0fd0b6d97b3b4715b5b8c825d3 Mon Sep 17 00:00:00 2001
From: dan <dan.abramov@gmail.com>
Date: Thu, 3 Oct 2024 11:41:23 +0900
Subject: Refactor video uploads (#5570)

* Remove unused video field

* Stop exposing video dispatch

* Move cancellation out of the reducer

* Make useUploadStatusQuery controlled by jobId

* Rename SetStatus to SetProcessing

This action only has one callsite and it's always passing "processing".

* Move jobId into video reducer state

* Make cancellation scoped

* Inline useCompressVideoMutation

* Move processVideo down the file

* Extract getErrorMessage

* useServiceAuthToken -> getServiceAuthToken

* useVideoAgent -> createVideoAgent

* useVideoUploadLimits -> getVideoUploadLimits

* useUploadVideoMutation -> uploadVideo

* Use async/await in processVideo

* Inline onVideoCompressed into processVideo

* Use async/await for uploadVideo

* Factor out error messages

* Guard dispatch with signal

This lets us remove the scattered signal checks around dispatch.

* Move job polling out of RQ

* Handle poll failures

* Remove unnecessary guards

* Slightly more accurate condition

* Move initVideoUri handling out of the hook

* Remove dead argument

It wasn't being used before either.

* Remove unused detailed status

This isn't being used because we're only respecting that state variable when isProcessing=true, but isProcessing is always false during video upload.

If we want to re-add this later, it should really just be derived from the reducer state.

* Harden the video reducer

* Tie all spawned work to a signal

* Preserve asset/media for nicer error state

* Rename actions to match states

* Inline useUploadVideo

This abstraction is getting in the way of some future work.

* Move MIME check to the only place that handles it
---
 src/view/com/composer/Composer.tsx | 124 +++++++++++++++++++++++--------------
 1 file changed, 78 insertions(+), 46 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 f354f0f0d..185a57fc3 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -36,6 +36,7 @@ import Animated, {
   ZoomOut,
 } from 'react-native-reanimated'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
+import {ImagePickerAsset} from 'expo-image-picker'
 import {
   AppBskyFeedDefs,
   AppBskyFeedGetPostThread,
@@ -82,9 +83,10 @@ import {Gif} from '#/state/queries/tenor'
 import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
 import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate/util'
 import {
+  createVideoState,
+  processVideo,
   State as VideoUploadState,
-  useUploadVideo,
-  VideoUploadDispatch,
+  videoReducer,
 } from '#/state/queries/video/video'
 import {useAgent, useSession} from '#/state/session'
 import {useComposerControls} from '#/state/shell/composer'
@@ -147,7 +149,8 @@ export const ComposePost = ({
 }) => {
   const {currentAccount} = useSession()
   const agent = useAgent()
-  const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
+  const currentDid = currentAccount!.did
+  const {data: currentProfile} = useProfileQuery({did: currentDid})
   const {isModalActive} = useModals()
   const {closeComposer} = useComposerControls()
   const pal = usePalette('default')
@@ -189,21 +192,50 @@ export const ComposePost = ({
   const [videoAltText, setVideoAltText] = useState('')
   const [captions, setCaptions] = useState<{lang: string; file: File}[]>([])
 
-  const {
-    selectVideo,
-    clearVideo,
-    state: videoUploadState,
-    updateVideoDimensions,
-    dispatch: videoUploadDispatch,
-  } = useUploadVideo({
-    setStatus: setProcessingState,
-    onSuccess: () => {
-      if (publishOnUpload) {
-        onPressPublish(true)
-      }
+  const [videoUploadState, videoDispatch] = useReducer(
+    videoReducer,
+    undefined,
+    createVideoState,
+  )
+
+  const selectVideo = React.useCallback(
+    (asset: ImagePickerAsset) => {
+      processVideo(
+        asset,
+        videoDispatch,
+        agent,
+        currentDid,
+        videoUploadState.abortController.signal,
+        _,
+      )
     },
-    initialVideoUri: initVideoUri,
-  })
+    [_, videoUploadState.abortController, videoDispatch, agent, currentDid],
+  )
+
+  // Whenever we receive an initial video uri, we should immediately run compression if necessary
+  useEffect(() => {
+    if (initVideoUri) {
+      selectVideo({uri: initVideoUri} as ImagePickerAsset)
+    }
+  }, [initVideoUri, selectVideo])
+
+  const clearVideo = React.useCallback(() => {
+    videoUploadState.abortController.abort()
+    videoDispatch({type: 'to_idle', nextController: new AbortController()})
+  }, [videoUploadState.abortController, videoDispatch])
+
+  const updateVideoDimensions = useCallback(
+    (width: number, height: number) => {
+      videoDispatch({
+        type: 'update_dimensions',
+        width,
+        height,
+        signal: videoUploadState.abortController.signal,
+      })
+    },
+    [videoUploadState.abortController],
+  )
+
   const hasVideo = Boolean(videoUploadState.asset || videoUploadState.video)
 
   const [publishOnUpload, setPublishOnUpload] = useState(false)
@@ -400,19 +432,18 @@ export const ComposePost = ({
             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,
+            video:
+              videoUploadState.status === 'done'
+                ? {
+                    blobRef: videoUploadState.pendingPublish.blobRef,
+                    altText: videoAltText,
+                    captions: captions,
+                    aspectRatio: {
+                      width: videoUploadState.asset.width,
+                      height: videoUploadState.asset.height,
+                    },
+                  }
+                : undefined,
           })
         ).uri
         try {
@@ -694,7 +725,7 @@ export const ComposePost = ({
             error={error}
             videoUploadState={videoUploadState}
             clearError={() => setError('')}
-            videoUploadDispatch={videoUploadDispatch}
+            clearVideo={clearVideo}
           />
         </Animated.View>
         <Animated.ScrollView
@@ -1083,25 +1114,25 @@ function ErrorBanner({
   error: standardError,
   videoUploadState,
   clearError,
-  videoUploadDispatch,
+  clearVideo,
 }: {
   error: string
   videoUploadState: VideoUploadState
   clearError: () => void
-  videoUploadDispatch: VideoUploadDispatch
+  clearVideo: () => void
 }) {
   const t = useTheme()
   const {_} = useLingui()
 
   const videoError =
-    videoUploadState.status !== 'idle' ? videoUploadState.error : undefined
+    videoUploadState.status === 'error' ? videoUploadState.error : undefined
   const error = standardError || videoError
 
   const onClearError = () => {
     if (standardError) {
       clearError()
     } else {
-      videoUploadDispatch({type: 'Reset'})
+      clearVideo()
     }
   }
 
@@ -1136,7 +1167,7 @@ function ErrorBanner({
             <ButtonIcon icon={X} />
           </Button>
         </View>
-        {videoError && videoUploadState.jobStatus?.jobId && (
+        {videoError && videoUploadState.jobId && (
           <NewText
             style={[
               {paddingLeft: 28},
@@ -1145,7 +1176,7 @@ function ErrorBanner({
               a.leading_snug,
               t.atoms.text_contrast_low,
             ]}>
-            <Trans>Job ID: {videoUploadState.jobStatus.jobId}</Trans>
+            <Trans>Job ID: {videoUploadState.jobId}</Trans>
           </NewText>
         )}
       </View>
@@ -1174,9 +1205,7 @@ function ToolbarWrapper({
 function VideoUploadToolbar({state}: {state: VideoUploadState}) {
   const t = useTheme()
   const {_} = useLingui()
-  const progress = state.jobStatus?.progress
-    ? state.jobStatus.progress / 100
-    : state.progress
+  const progress = state.progress
   const shouldRotate =
     state.status === 'processing' && (progress === 0 || progress === 1)
   let wheelProgress = shouldRotate ? 0.33 : progress
@@ -1212,16 +1241,15 @@ function VideoUploadToolbar({state}: {state: VideoUploadState}) {
     case 'processing':
       text = _('Processing video...')
       break
+    case 'error':
+      text = _('Error')
+      wheelProgress = 100
+      break
     case 'done':
       text = _('Video uploaded')
       break
   }
 
-  if (state.error) {
-    text = _('Error')
-    wheelProgress = 100
-  }
-
   return (
     <ToolbarWrapper style={[a.flex_row, a.align_center, {paddingVertical: 5}]}>
       <Animated.View style={[animatedStyle]}>
@@ -1229,7 +1257,11 @@ function VideoUploadToolbar({state}: {state: VideoUploadState}) {
           size={30}
           borderWidth={1}
           borderColor={t.atoms.border_contrast_low.borderColor}
-          color={state.error ? t.palette.negative_500 : t.palette.primary_500}
+          color={
+            state.status === 'error'
+              ? t.palette.negative_500
+              : t.palette.primary_500
+          }
           progress={wheelProgress}
         />
       </Animated.View>
-- 
cgit 1.4.1

