Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manage video reducer from composer reducer #5573

Merged
merged 5 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 26 additions & 41 deletions src/state/queries/video/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -114,28 +113,29 @@ type DoneState = {
pendingPublish: {blobRef: BlobRef; mutableProcessed: boolean}
}

export type State =
| IdleState
export type VideoState =
| ErrorState
| CompressingState
| UploadingState
| ProcessingState
| 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
Expand All @@ -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 {
Expand Down Expand Up @@ -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, {
Expand Down
49 changes: 29 additions & 20 deletions src/view/com/composer/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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) => {
Expand All @@ -233,7 +248,7 @@ export const ComposePost = ({
signal: videoUploadState.abortController.signal,
})
},
[videoUploadState.abortController],
[videoUploadState.abortController, videoDispatch],
)

const hasVideo = Boolean(videoUploadState.asset || videoUploadState.video)
Expand All @@ -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
Expand Down Expand Up @@ -857,7 +866,7 @@ export const ComposePost = ({
/>
<SelectVideoBtn
onSelectVideo={selectVideo}
disabled={!canSelectImages}
disabled={!canSelectImages || images?.length > 0}
setError={setError}
/>
<OpenCameraBtn disabled={!canSelectImages} onAdd={onImageAdd} />
Expand Down Expand Up @@ -1117,7 +1126,7 @@ function ErrorBanner({
clearVideo,
}: {
error: string
videoUploadState: VideoUploadState
videoUploadState: VideoUploadState | NoVideoState
clearError: () => void
clearVideo: () => void
}) {
Expand Down
75 changes: 74 additions & 1 deletion src/view/com/composer/state.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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 = {
Expand All @@ -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

Expand All @@ -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) {
Expand Down Expand Up @@ -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),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

composition :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Niiice

}
}
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
}
Expand All @@ -122,6 +194,7 @@ export function createComposerState({
labels: [],
}
}
// TODO: initial video.
return {
embed: {
record: undefined,
Expand Down
Loading