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
This commit is contained in:
dan
2024-10-03 14:26:38 +09:00
committed by GitHub
parent d2392d2d64
commit 03704e2b48
3 changed files with 129 additions and 62 deletions
+26 -41
View File
@@ -16,13 +16,7 @@ import {logger} from '#/logger'
import {createVideoAgent} from '#/state/queries/video/util' import {createVideoAgent} from '#/state/queries/video/util'
import {uploadVideo} from '#/state/queries/video/video-upload' import {uploadVideo} from '#/state/queries/video/video-upload'
type Action = export type VideoAction =
| {type: 'to_idle'; nextController: AbortController}
| {
type: 'idle_to_compressing'
asset: ImagePickerAsset
signal: AbortSignal
}
| { | {
type: 'compressing_to_uploading' type: 'compressing_to_uploading'
video: CompressedVideo video: CompressedVideo
@@ -52,15 +46,20 @@ type Action =
signal: AbortSignal signal: AbortSignal
} }
type IdleState = { const noopController = new AbortController()
status: 'idle' noopController.abort()
progress: 0
abortController: AbortController export const NO_VIDEO = Object.freeze({
asset?: undefined status: 'idle',
video?: undefined progress: 0,
jobId?: undefined abortController: noopController,
pendingPublish?: undefined asset: undefined,
} video: undefined,
jobId: undefined,
pendingPublish: undefined,
})
export type NoVideoState = typeof NO_VIDEO
type ErrorState = { type ErrorState = {
status: 'error' status: 'error'
@@ -114,8 +113,7 @@ type DoneState = {
pendingPublish: {blobRef: BlobRef; mutableProcessed: boolean} pendingPublish: {blobRef: BlobRef; mutableProcessed: boolean}
} }
export type State = export type VideoState =
| IdleState
| ErrorState | ErrorState
| CompressingState | CompressingState
| UploadingState | UploadingState
@@ -123,19 +121,21 @@ export type State =
| DoneState | DoneState
export function createVideoState( export function createVideoState(
abortController: AbortController = new AbortController(), asset: ImagePickerAsset,
): IdleState { abortController: AbortController,
): CompressingState {
return { return {
status: 'idle', status: 'compressing',
progress: 0, progress: 0,
abortController, abortController,
asset,
} }
} }
export function videoReducer(state: State, action: Action): State { export function videoReducer(
if (action.type === 'to_idle') { state: VideoState,
return createVideoState(action.nextController) action: VideoAction,
} ): VideoState {
if (action.signal.aborted || action.signal !== state.abortController.signal) { if (action.signal.aborted || action.signal !== state.abortController.signal) {
// This action is stale and the process that spawned it is no longer relevant. // This action is stale and the process that spawned it is no longer relevant.
return state return state
@@ -157,15 +157,6 @@ export function videoReducer(state: State, action: Action): State {
progress: action.progress, 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') { } else if (action.type === 'update_dimensions') {
if (state.asset) { if (state.asset) {
return { return {
@@ -238,18 +229,12 @@ function trunc2dp(num: number) {
export async function processVideo( export async function processVideo(
asset: ImagePickerAsset, asset: ImagePickerAsset,
dispatch: (action: Action) => void, dispatch: (action: VideoAction) => void,
agent: BskyAgent, agent: BskyAgent,
did: string, did: string,
signal: AbortSignal, signal: AbortSignal,
_: I18n['_'], _: I18n['_'],
) { ) {
dispatch({
type: 'idle_to_compressing',
asset,
signal,
})
let video: CompressedVideo | undefined let video: CompressedVideo | undefined
try { try {
video = await compressVideo(asset, { video = await compressVideo(asset, {
+29 -20
View File
@@ -82,11 +82,12 @@ import {useProfileQuery} from '#/state/queries/profile'
import {Gif} from '#/state/queries/tenor' import {Gif} from '#/state/queries/tenor'
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate' import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate/util' import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate/util'
import {NO_VIDEO, NoVideoState} from '#/state/queries/video/video'
import { import {
createVideoState,
processVideo, processVideo,
State as VideoUploadState, VideoAction,
videoReducer, VideoState,
VideoState as VideoUploadState,
} from '#/state/queries/video/video' } from '#/state/queries/video/video'
import {useAgent, useSession} from '#/state/session' import {useAgent, useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer' import {useComposerControls} from '#/state/shell/composer'
@@ -192,24 +193,38 @@ export const ComposePost = ({
const [videoAltText, setVideoAltText] = useState('') const [videoAltText, setVideoAltText] = useState('')
const [captions, setCaptions] = useState<{lang: string; file: File}[]>([]) const [captions, setCaptions] = useState<{lang: string; file: File}[]>([])
const [videoUploadState, videoDispatch] = useReducer( // TODO: Move more state here.
videoReducer, const [composerState, dispatch] = useReducer(
undefined, composerReducer,
createVideoState, {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( const selectVideo = React.useCallback(
(asset: ImagePickerAsset) => { (asset: ImagePickerAsset) => {
const abortController = new AbortController()
dispatch({type: 'embed_add_video', asset, abortController})
processVideo( processVideo(
asset, asset,
videoDispatch, videoDispatch,
agent, agent,
currentDid, 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 // 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(() => { const clearVideo = React.useCallback(() => {
videoUploadState.abortController.abort() videoUploadState.abortController.abort()
videoDispatch({type: 'to_idle', nextController: new AbortController()}) dispatch({type: 'embed_remove_video'})
}, [videoUploadState.abortController, videoDispatch]) }, [videoUploadState.abortController, dispatch])
const updateVideoDimensions = useCallback( const updateVideoDimensions = useCallback(
(width: number, height: number) => { (width: number, height: number) => {
@@ -233,7 +248,7 @@ export const ComposePost = ({
signal: videoUploadState.abortController.signal, signal: videoUploadState.abortController.signal,
}) })
}, },
[videoUploadState.abortController], [videoUploadState.abortController, videoDispatch],
) )
const hasVideo = Boolean(videoUploadState.asset || videoUploadState.video) const hasVideo = Boolean(videoUploadState.asset || videoUploadState.video)
@@ -249,12 +264,6 @@ export const ComposePost = ({
) )
const [postgate, setPostgate] = useState(createPostgateRecord({post: ''})) const [postgate, setPostgate] = useState(createPostgateRecord({post: ''}))
// TODO: Move more state here.
const [composerState, dispatch] = useReducer(
composerReducer,
{initImageUris},
createComposerState,
)
let images = NO_IMAGES let images = NO_IMAGES
if (composerState.embed.media?.type === 'images') { if (composerState.embed.media?.type === 'images') {
images = composerState.embed.media.images images = composerState.embed.media.images
@@ -857,7 +866,7 @@ export const ComposePost = ({
/> />
<SelectVideoBtn <SelectVideoBtn
onSelectVideo={selectVideo} onSelectVideo={selectVideo}
disabled={!canSelectImages} disabled={!canSelectImages || images?.length > 0}
setError={setError} setError={setError}
/> />
<OpenCameraBtn disabled={!canSelectImages} onAdd={onImageAdd} /> <OpenCameraBtn disabled={!canSelectImages} onAdd={onImageAdd} />
@@ -1117,7 +1126,7 @@ function ErrorBanner({
clearVideo, clearVideo,
}: { }: {
error: string error: string
videoUploadState: VideoUploadState videoUploadState: VideoUploadState | NoVideoState
clearError: () => void clearError: () => void
clearVideo: () => void clearVideo: () => void
}) { }) {
+74 -1
View File
@@ -1,4 +1,12 @@
import {ImagePickerAsset} from 'expo-image-picker'
import {ComposerImage, createInitialImages} from '#/state/gallery' import {ComposerImage, createInitialImages} from '#/state/gallery'
import {
createVideoState,
VideoAction,
videoReducer,
VideoState,
} from '#/state/queries/video/video'
import {ComposerOpts} from '#/state/shell/composer' import {ComposerOpts} from '#/state/shell/composer'
type PostRecord = { type PostRecord = {
@@ -11,11 +19,16 @@ type ImagesMedia = {
labels: string[] labels: string[]
} }
type VideoMedia = {
type: 'video'
video: VideoState
}
type ComposerEmbed = { type ComposerEmbed = {
// TODO: Other record types. // TODO: Other record types.
record: PostRecord | undefined record: PostRecord | undefined
// TODO: Other media types. // TODO: Other media types.
media: ImagesMedia | undefined media: ImagesMedia | VideoMedia | undefined
} }
export type ComposerState = { export type ComposerState = {
@@ -27,6 +40,13 @@ export type ComposerAction =
| {type: 'embed_add_images'; images: ComposerImage[]} | {type: 'embed_add_images'; images: ComposerImage[]}
| {type: 'embed_update_image'; image: ComposerImage} | {type: 'embed_update_image'; image: ComposerImage}
| {type: 'embed_remove_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 const MAX_IMAGES = 4
@@ -36,6 +56,9 @@ export function composerReducer(
): ComposerState { ): ComposerState {
switch (action.type) { switch (action.type) {
case 'embed_add_images': { case 'embed_add_images': {
if (action.images.length === 0) {
return state
}
const prevMedia = state.embed.media const prevMedia = state.embed.media
let nextMedia = prevMedia let nextMedia = prevMedia
if (!prevMedia) { if (!prevMedia) {
@@ -104,6 +127,55 @@ export function composerReducer(
} }
return state 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: default:
return state return state
} }
@@ -122,6 +194,7 @@ export function createComposerState({
labels: [], labels: [],
} }
} }
// TODO: initial video.
return { return {
embed: { embed: {
record: undefined, record: undefined,