about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2024-10-03 14:26:38 +0900
committerGitHub <noreply@github.com>2024-10-03 14:26:38 +0900
commit03704e2b48e6cdc348ce7277f2bcae0c61519d1e (patch)
treec7734f0345ed821a010ba49be6713b7b436e88b9 /src
parentd2392d2d64c46d0fd0b6d97b3b4715b5b8c825d3 (diff)
downloadvoidsky-03704e2b48e6cdc348ce7277f2bcae0c61519d1e.tar.zst
Manage video reducer from composer reducer (#5573)
* Move video state into composer state

* Represent video as embed

This is slightly broken. In particular, we can't remove video yet because there's no action that results in video embed being removed.

* Properly represent video as embed

This aligns the video state lifetime with the embed lifetime. Video can now be properly added and removed.

* Disable Add Video when we have images

* Ignore empty image pick
Diffstat (limited to 'src')
-rw-r--r--src/state/queries/video/video.ts67
-rw-r--r--src/view/com/composer/Composer.tsx49
-rw-r--r--src/view/com/composer/state.ts75
3 files changed, 129 insertions, 62 deletions
diff --git a/src/state/queries/video/video.ts b/src/state/queries/video/video.ts
index fabee6ad1..dbbb6c202 100644
--- a/src/state/queries/video/video.ts
+++ b/src/state/queries/video/video.ts
@@ -16,13 +16,7 @@ import {logger} from '#/logger'
 import {createVideoAgent} from '#/state/queries/video/util'
 import {uploadVideo} from '#/state/queries/video/video-upload'
 
-type Action =
-  | {type: 'to_idle'; nextController: AbortController}
-  | {
-      type: 'idle_to_compressing'
-      asset: ImagePickerAsset
-      signal: AbortSignal
-    }
+export type VideoAction =
   | {
       type: 'compressing_to_uploading'
       video: CompressedVideo
@@ -52,15 +46,20 @@ type Action =
       signal: AbortSignal
     }
 
-type IdleState = {
-  status: 'idle'
-  progress: 0
-  abortController: AbortController
-  asset?: undefined
-  video?: undefined
-  jobId?: undefined
-  pendingPublish?: undefined
-}
+const noopController = new AbortController()
+noopController.abort()
+
+export const NO_VIDEO = Object.freeze({
+  status: 'idle',
+  progress: 0,
+  abortController: noopController,
+  asset: undefined,
+  video: undefined,
+  jobId: undefined,
+  pendingPublish: undefined,
+})
+
+export type NoVideoState = typeof NO_VIDEO
 
 type ErrorState = {
   status: 'error'
@@ -114,8 +113,7 @@ type DoneState = {
   pendingPublish: {blobRef: BlobRef; mutableProcessed: boolean}
 }
 
-export type State =
-  | IdleState
+export type VideoState =
   | ErrorState
   | CompressingState
   | UploadingState
@@ -123,19 +121,21 @@ export type State =
   | DoneState
 
 export function createVideoState(
-  abortController: AbortController = new AbortController(),
-): IdleState {
+  asset: ImagePickerAsset,
+  abortController: AbortController,
+): CompressingState {
   return {
-    status: 'idle',
+    status: 'compressing',
     progress: 0,
     abortController,
+    asset,
   }
 }
 
-export function videoReducer(state: State, action: Action): State {
-  if (action.type === 'to_idle') {
-    return createVideoState(action.nextController)
-  }
+export function videoReducer(
+  state: VideoState,
+  action: VideoAction,
+): VideoState {
   if (action.signal.aborted || action.signal !== state.abortController.signal) {
     // This action is stale and the process that spawned it is no longer relevant.
     return state
@@ -157,15 +157,6 @@ export function videoReducer(state: State, action: Action): State {
         progress: action.progress,
       }
     }
-  } else if (action.type === 'idle_to_compressing') {
-    if (state.status === 'idle') {
-      return {
-        status: 'compressing',
-        progress: 0,
-        abortController: state.abortController,
-        asset: action.asset,
-      }
-    }
   } else if (action.type === 'update_dimensions') {
     if (state.asset) {
       return {
@@ -238,18 +229,12 @@ function trunc2dp(num: number) {
 
 export async function processVideo(
   asset: ImagePickerAsset,
-  dispatch: (action: Action) => void,
+  dispatch: (action: VideoAction) => void,
   agent: BskyAgent,
   did: string,
   signal: AbortSignal,
   _: I18n['_'],
 ) {
-  dispatch({
-    type: 'idle_to_compressing',
-    asset,
-    signal,
-  })
-
   let video: CompressedVideo | undefined
   try {
     video = await compressVideo(asset, {
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 185a57fc3..59aae2951 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -82,11 +82,12 @@ import {useProfileQuery} from '#/state/queries/profile'
 import {Gif} from '#/state/queries/tenor'
 import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
 import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate/util'
+import {NO_VIDEO, NoVideoState} from '#/state/queries/video/video'
 import {
-  createVideoState,
   processVideo,
-  State as VideoUploadState,
-  videoReducer,
+  VideoAction,
+  VideoState,
+  VideoState as VideoUploadState,
 } from '#/state/queries/video/video'
 import {useAgent, useSession} from '#/state/session'
 import {useComposerControls} from '#/state/shell/composer'
@@ -192,24 +193,38 @@ export const ComposePost = ({
   const [videoAltText, setVideoAltText] = useState('')
   const [captions, setCaptions] = useState<{lang: string; file: File}[]>([])
 
-  const [videoUploadState, videoDispatch] = useReducer(
-    videoReducer,
-    undefined,
-    createVideoState,
+  // TODO: Move more state here.
+  const [composerState, dispatch] = useReducer(
+    composerReducer,
+    {initImageUris},
+    createComposerState,
+  )
+
+  let videoUploadState: VideoState | NoVideoState = NO_VIDEO
+  if (composerState.embed.media?.type === 'video') {
+    videoUploadState = composerState.embed.media.video
+  }
+  const videoDispatch = useCallback(
+    (videoAction: VideoAction) => {
+      dispatch({type: 'embed_update_video', videoAction})
+    },
+    [dispatch],
   )
 
   const selectVideo = React.useCallback(
     (asset: ImagePickerAsset) => {
+      const abortController = new AbortController()
+      dispatch({type: 'embed_add_video', asset, abortController})
       processVideo(
         asset,
         videoDispatch,
         agent,
         currentDid,
-        videoUploadState.abortController.signal,
+        abortController.signal,
         _,
       )
     },
-    [_, videoUploadState.abortController, videoDispatch, agent, currentDid],
+    [_, videoDispatch, agent, currentDid],
   )
 
   // Whenever we receive an initial video uri, we should immediately run compression if necessary
@@ -221,8 +236,8 @@ export const ComposePost = ({
 
   const clearVideo = React.useCallback(() => {
     videoUploadState.abortController.abort()
-    videoDispatch({type: 'to_idle', nextController: new AbortController()})
-  }, [videoUploadState.abortController, videoDispatch])
+    dispatch({type: 'embed_remove_video'})
+  }, [videoUploadState.abortController, dispatch])
 
   const updateVideoDimensions = useCallback(
     (width: number, height: number) => {
@@ -233,7 +248,7 @@ export const ComposePost = ({
         signal: videoUploadState.abortController.signal,
       })
     },
-    [videoUploadState.abortController],
+    [videoUploadState.abortController, videoDispatch],
   )
 
   const hasVideo = Boolean(videoUploadState.asset || videoUploadState.video)
@@ -249,12 +264,6 @@ export const ComposePost = ({
     )
   const [postgate, setPostgate] = useState(createPostgateRecord({post: ''}))
 
-  // TODO: Move more state here.
-  const [composerState, dispatch] = useReducer(
-    composerReducer,
-    {initImageUris},
-    createComposerState,
-  )
   let images = NO_IMAGES
   if (composerState.embed.media?.type === 'images') {
     images = composerState.embed.media.images
@@ -857,7 +866,7 @@ export const ComposePost = ({
               />
               <SelectVideoBtn
                 onSelectVideo={selectVideo}
-                disabled={!canSelectImages}
+                disabled={!canSelectImages || images?.length > 0}
                 setError={setError}
               />
               <OpenCameraBtn disabled={!canSelectImages} onAdd={onImageAdd} />
@@ -1117,7 +1126,7 @@ function ErrorBanner({
   clearVideo,
 }: {
   error: string
-  videoUploadState: VideoUploadState
+  videoUploadState: VideoUploadState | NoVideoState
   clearError: () => void
   clearVideo: () => void
 }) {
diff --git a/src/view/com/composer/state.ts b/src/view/com/composer/state.ts
index 5588de1aa..8e974ad7a 100644
--- a/src/view/com/composer/state.ts
+++ b/src/view/com/composer/state.ts
@@ -1,4 +1,12 @@
+import {ImagePickerAsset} from 'expo-image-picker'
+
 import {ComposerImage, createInitialImages} from '#/state/gallery'
+import {
+  createVideoState,
+  VideoAction,
+  videoReducer,
+  VideoState,
+} from '#/state/queries/video/video'
 import {ComposerOpts} from '#/state/shell/composer'
 
 type PostRecord = {
@@ -11,11 +19,16 @@ type ImagesMedia = {
   labels: string[]
 }
 
+type VideoMedia = {
+  type: 'video'
+  video: VideoState
+}
+
 type ComposerEmbed = {
   // TODO: Other record types.
   record: PostRecord | undefined
   // TODO: Other media types.
-  media: ImagesMedia | undefined
+  media: ImagesMedia | VideoMedia | undefined
 }
 
 export type ComposerState = {
@@ -27,6 +40,13 @@ export type ComposerAction =
   | {type: 'embed_add_images'; images: ComposerImage[]}
   | {type: 'embed_update_image'; image: ComposerImage}
   | {type: 'embed_remove_image'; image: ComposerImage}
+  | {
+      type: 'embed_add_video'
+      asset: ImagePickerAsset
+      abortController: AbortController
+    }
+  | {type: 'embed_remove_video'}
+  | {type: 'embed_update_video'; videoAction: VideoAction}
 
 const MAX_IMAGES = 4
 
@@ -36,6 +56,9 @@ export function composerReducer(
 ): ComposerState {
   switch (action.type) {
     case 'embed_add_images': {
+      if (action.images.length === 0) {
+        return state
+      }
       const prevMedia = state.embed.media
       let nextMedia = prevMedia
       if (!prevMedia) {
@@ -104,6 +127,55 @@ export function composerReducer(
       }
       return state
     }
+    case 'embed_add_video': {
+      const prevMedia = state.embed.media
+      let nextMedia = prevMedia
+      if (!prevMedia) {
+        nextMedia = {
+          type: 'video',
+          video: createVideoState(action.asset, action.abortController),
+        }
+      }
+      return {
+        ...state,
+        embed: {
+          ...state.embed,
+          media: nextMedia,
+        },
+      }
+    }
+    case 'embed_update_video': {
+      const videoAction = action.videoAction
+      const prevMedia = state.embed.media
+      let nextMedia = prevMedia
+      if (prevMedia?.type === 'video') {
+        nextMedia = {
+          ...prevMedia,
+          video: videoReducer(prevMedia.video, videoAction),
+        }
+      }
+      return {
+        ...state,
+        embed: {
+          ...state.embed,
+          media: nextMedia,
+        },
+      }
+    }
+    case 'embed_remove_video': {
+      const prevMedia = state.embed.media
+      let nextMedia = prevMedia
+      if (prevMedia?.type === 'video') {
+        nextMedia = undefined
+      }
+      return {
+        ...state,
+        embed: {
+          ...state.embed,
+          media: nextMedia,
+        },
+      }
+    }
     default:
       return state
   }
@@ -122,6 +194,7 @@ export function createComposerState({
       labels: [],
     }
   }
+  // TODO: initial video.
   return {
     embed: {
       record: undefined,