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:
@@ -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, {
|
||||||
|
|||||||
@@ -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
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user