about summary refs log tree commit diff
path: root/src/view/com/composer/state
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/composer/state')
-rw-r--r--src/view/com/composer/state/composer.ts151
-rw-r--r--src/view/com/composer/state/video.ts48
2 files changed, 182 insertions, 17 deletions
diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts
index a23a5d8c8..769a0521d 100644
--- a/src/view/com/composer/state/composer.ts
+++ b/src/view/com/composer/state/composer.ts
@@ -1,17 +1,14 @@
 import {ImagePickerAsset} from 'expo-image-picker'
 
+import {isBskyPostUrl} from '#/lib/strings/url-helpers'
 import {ComposerImage, createInitialImages} from '#/state/gallery'
+import {Gif} from '#/state/queries/tenor'
 import {ComposerOpts} from '#/state/shell/composer'
 import {createVideoState, VideoAction, videoReducer, VideoState} from './video'
 
-type PostRecord = {
-  uri: string
-}
-
 type ImagesMedia = {
   type: 'images'
   images: ComposerImage[]
-  labels: string[]
 }
 
 type VideoMedia = {
@@ -19,16 +16,30 @@ type VideoMedia = {
   video: VideoState
 }
 
-type ComposerEmbed = {
-  // TODO: Other record types.
-  record: PostRecord | undefined
-  // TODO: Other media types.
-  media: ImagesMedia | VideoMedia | undefined
+type GifMedia = {
+  type: 'gif'
+  gif: Gif
+  alt: string
+}
+
+type Link = {
+  type: 'link'
+  uri: string
+}
+
+// This structure doesn't exactly correspond to the data model.
+// Instead, it maps to how the UI is organized, and how we present a post.
+type EmbedDraft = {
+  // We'll always submit quote and actual media (images, video, gifs) chosen by the user.
+  quote: Link | undefined
+  media: ImagesMedia | VideoMedia | GifMedia | undefined
+  // This field may end up ignored if we have more important things to display than a link card:
+  link: Link | undefined
 }
 
 export type ComposerState = {
   // TODO: Other draft data.
-  embed: ComposerEmbed
+  embed: EmbedDraft
 }
 
 export type ComposerAction =
@@ -42,6 +53,12 @@ export type ComposerAction =
     }
   | {type: 'embed_remove_video'}
   | {type: 'embed_update_video'; videoAction: VideoAction}
+  | {type: 'embed_add_uri'; uri: string}
+  | {type: 'embed_remove_quote'}
+  | {type: 'embed_remove_link'}
+  | {type: 'embed_add_gif'; gif: Gif}
+  | {type: 'embed_update_gif'; alt: string}
+  | {type: 'embed_remove_gif'}
 
 const MAX_IMAGES = 4
 
@@ -60,7 +77,6 @@ export function composerReducer(
         nextMedia = {
           type: 'images',
           images: action.images.slice(0, MAX_IMAGES),
-          labels: [],
         }
       } else if (prevMedia.type === 'images') {
         nextMedia = {
@@ -171,6 +187,102 @@ export function composerReducer(
         },
       }
     }
+    case 'embed_add_uri': {
+      const prevQuote = state.embed.quote
+      const prevLink = state.embed.link
+      let nextQuote = prevQuote
+      let nextLink = prevLink
+      if (isBskyPostUrl(action.uri)) {
+        if (!prevQuote) {
+          nextQuote = {
+            type: 'link',
+            uri: action.uri,
+          }
+        }
+      } else {
+        if (!prevLink) {
+          nextLink = {
+            type: 'link',
+            uri: action.uri,
+          }
+        }
+      }
+      return {
+        ...state,
+        embed: {
+          ...state.embed,
+          quote: nextQuote,
+          link: nextLink,
+        },
+      }
+    }
+    case 'embed_remove_link': {
+      return {
+        ...state,
+        embed: {
+          ...state.embed,
+          link: undefined,
+        },
+      }
+    }
+    case 'embed_remove_quote': {
+      return {
+        ...state,
+        embed: {
+          ...state.embed,
+          quote: undefined,
+        },
+      }
+    }
+    case 'embed_add_gif': {
+      const prevMedia = state.embed.media
+      let nextMedia = prevMedia
+      if (!prevMedia) {
+        nextMedia = {
+          type: 'gif',
+          gif: action.gif,
+          alt: '',
+        }
+      }
+      return {
+        ...state,
+        embed: {
+          ...state.embed,
+          media: nextMedia,
+        },
+      }
+    }
+    case 'embed_update_gif': {
+      const prevMedia = state.embed.media
+      let nextMedia = prevMedia
+      if (prevMedia?.type === 'gif') {
+        nextMedia = {
+          ...prevMedia,
+          alt: action.alt,
+        }
+      }
+      return {
+        ...state,
+        embed: {
+          ...state.embed,
+          media: nextMedia,
+        },
+      }
+    }
+    case 'embed_remove_gif': {
+      const prevMedia = state.embed.media
+      let nextMedia = prevMedia
+      if (prevMedia?.type === 'gif') {
+        nextMedia = undefined
+      }
+      return {
+        ...state,
+        embed: {
+          ...state.embed,
+          media: nextMedia,
+        },
+      }
+    }
     default:
       return state
   }
@@ -178,22 +290,31 @@ export function composerReducer(
 
 export function createComposerState({
   initImageUris,
+  initQuoteUri,
 }: {
   initImageUris: ComposerOpts['imageUris']
+  initQuoteUri: string | undefined
 }): ComposerState {
   let media: ImagesMedia | undefined
   if (initImageUris?.length) {
     media = {
       type: 'images',
       images: createInitialImages(initImageUris),
-      labels: [],
     }
   }
-  // TODO: initial video.
+  let quote: Link | undefined
+  if (initQuoteUri) {
+    quote = {
+      type: 'link',
+      uri: initQuoteUri,
+    }
+  }
+  // TODO: Other initial content.
   return {
     embed: {
-      record: undefined,
+      quote,
       media,
+      link: undefined,
     },
   }
 }
diff --git a/src/view/com/composer/state/video.ts b/src/view/com/composer/state/video.ts
index 269505657..e29687200 100644
--- a/src/view/com/composer/state/video.ts
+++ b/src/view/com/composer/state/video.ts
@@ -4,8 +4,6 @@ import {JobStatus} from '@atproto/api/dist/client/types/app/bsky/video/defs'
 import {I18n} from '@lingui/core'
 import {msg} from '@lingui/macro'
 
-import {createVideoAgent} from '#/lib/media/video/util'
-import {uploadVideo} from '#/lib/media/video/upload'
 import {AbortError} from '#/lib/async/cancelable'
 import {compressVideo} from '#/lib/media/video/compress'
 import {
@@ -14,8 +12,12 @@ import {
   VideoTooLargeError,
 } from '#/lib/media/video/errors'
 import {CompressedVideo} from '#/lib/media/video/types'
+import {uploadVideo} from '#/lib/media/video/upload'
+import {createVideoAgent} from '#/lib/media/video/util'
 import {logger} from '#/logger'
 
+type CaptionsTrack = {lang: string; file: File}
+
 export type VideoAction =
   | {
       type: 'compressing_to_uploading'
@@ -41,6 +43,16 @@ export type VideoAction =
       signal: AbortSignal
     }
   | {
+      type: 'update_alt_text'
+      altText: string
+      signal: AbortSignal
+    }
+  | {
+      type: 'update_captions'
+      updater: (prev: CaptionsTrack[]) => CaptionsTrack[]
+      signal: AbortSignal
+    }
+  | {
       type: 'update_job_status'
       jobStatus: AppBskyVideoDefs.JobStatus
       signal: AbortSignal
@@ -57,6 +69,8 @@ export const NO_VIDEO = Object.freeze({
   video: undefined,
   jobId: undefined,
   pendingPublish: undefined,
+  altText: '',
+  captions: [],
 })
 
 export type NoVideoState = typeof NO_VIDEO
@@ -70,6 +84,8 @@ type ErrorState = {
   jobId: string | null
   error: string
   pendingPublish?: undefined
+  altText: string
+  captions: CaptionsTrack[]
 }
 
 type CompressingState = {
@@ -80,6 +96,8 @@ type CompressingState = {
   video?: undefined
   jobId?: undefined
   pendingPublish?: undefined
+  altText: string
+  captions: CaptionsTrack[]
 }
 
 type UploadingState = {
@@ -90,6 +108,8 @@ type UploadingState = {
   video: CompressedVideo
   jobId?: undefined
   pendingPublish?: undefined
+  altText: string
+  captions: CaptionsTrack[]
 }
 
 type ProcessingState = {
@@ -101,6 +121,8 @@ type ProcessingState = {
   jobId: string
   jobStatus: AppBskyVideoDefs.JobStatus | null
   pendingPublish?: undefined
+  altText: string
+  captions: CaptionsTrack[]
 }
 
 type DoneState = {
@@ -111,6 +133,8 @@ type DoneState = {
   video: CompressedVideo
   jobId?: undefined
   pendingPublish: {blobRef: BlobRef; mutableProcessed: boolean}
+  altText: string
+  captions: CaptionsTrack[]
 }
 
 export type VideoState =
@@ -129,6 +153,8 @@ export function createVideoState(
     progress: 0,
     abortController,
     asset,
+    altText: '',
+    captions: [],
   }
 }
 
@@ -149,6 +175,8 @@ export function videoReducer(
       asset: state.asset ?? null,
       video: state.video ?? null,
       jobId: state.jobId ?? null,
+      altText: state.altText,
+      captions: state.captions,
     }
   } else if (action.type === 'update_progress') {
     if (state.status === 'compressing' || state.status === 'uploading') {
@@ -164,6 +192,16 @@ export function videoReducer(
         asset: {...state.asset, width: action.width, height: action.height},
       }
     }
+  } else if (action.type === 'update_alt_text') {
+    return {
+      ...state,
+      altText: action.altText,
+    }
+  } else if (action.type === 'update_captions') {
+    return {
+      ...state,
+      captions: action.updater(state.captions),
+    }
   } else if (action.type === 'compressing_to_uploading') {
     if (state.status === 'compressing') {
       return {
@@ -172,6 +210,8 @@ export function videoReducer(
         abortController: state.abortController,
         asset: state.asset,
         video: action.video,
+        altText: state.altText,
+        captions: state.captions,
       }
     }
     return state
@@ -185,6 +225,8 @@ export function videoReducer(
         video: state.video,
         jobId: action.jobId,
         jobStatus: null,
+        altText: state.altText,
+        captions: state.captions,
       }
     }
   } else if (action.type === 'update_job_status') {
@@ -210,6 +252,8 @@ export function videoReducer(
           blobRef: action.blobRef,
           mutableProcessed: false,
         },
+        altText: state.altText,
+        captions: state.captions,
       }
     }
   }