Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

authored by

dan and committed by
GitHub
03704e2b d2392d2d

+129 -62
+26 -41
src/state/queries/video/video.ts
··· 16 16 import {createVideoAgent} from '#/state/queries/video/util' 17 17 import {uploadVideo} from '#/state/queries/video/video-upload' 18 18 19 - type Action = 20 - | {type: 'to_idle'; nextController: AbortController} 21 - | { 22 - type: 'idle_to_compressing' 23 - asset: ImagePickerAsset 24 - signal: AbortSignal 25 - } 19 + export type VideoAction = 26 20 | { 27 21 type: 'compressing_to_uploading' 28 22 video: CompressedVideo ··· 52 46 signal: AbortSignal 53 47 } 54 48 55 - type IdleState = { 56 - status: 'idle' 57 - progress: 0 58 - abortController: AbortController 59 - asset?: undefined 60 - video?: undefined 61 - jobId?: undefined 62 - pendingPublish?: undefined 63 - } 49 + const noopController = new AbortController() 50 + noopController.abort() 51 + 52 + export const NO_VIDEO = Object.freeze({ 53 + status: 'idle', 54 + progress: 0, 55 + abortController: noopController, 56 + asset: undefined, 57 + video: undefined, 58 + jobId: undefined, 59 + pendingPublish: undefined, 60 + }) 61 + 62 + export type NoVideoState = typeof NO_VIDEO 64 63 65 64 type ErrorState = { 66 65 status: 'error' ··· 114 113 pendingPublish: {blobRef: BlobRef; mutableProcessed: boolean} 115 114 } 116 115 117 - export type State = 118 - | IdleState 116 + export type VideoState = 119 117 | ErrorState 120 118 | CompressingState 121 119 | UploadingState ··· 123 121 | DoneState 124 122 125 123 export function createVideoState( 126 - abortController: AbortController = new AbortController(), 127 - ): IdleState { 124 + asset: ImagePickerAsset, 125 + abortController: AbortController, 126 + ): CompressingState { 128 127 return { 129 - status: 'idle', 128 + status: 'compressing', 130 129 progress: 0, 131 130 abortController, 131 + asset, 132 132 } 133 133 } 134 134 135 - export function videoReducer(state: State, action: Action): State { 136 - if (action.type === 'to_idle') { 137 - return createVideoState(action.nextController) 138 - } 135 + export function videoReducer( 136 + state: VideoState, 137 + action: VideoAction, 138 + ): VideoState { 139 139 if (action.signal.aborted || action.signal !== state.abortController.signal) { 140 140 // This action is stale and the process that spawned it is no longer relevant. 141 141 return state ··· 155 155 return { 156 156 ...state, 157 157 progress: action.progress, 158 - } 159 - } 160 - } else if (action.type === 'idle_to_compressing') { 161 - if (state.status === 'idle') { 162 - return { 163 - status: 'compressing', 164 - progress: 0, 165 - abortController: state.abortController, 166 - asset: action.asset, 167 158 } 168 159 } 169 160 } else if (action.type === 'update_dimensions') { ··· 238 229 239 230 export async function processVideo( 240 231 asset: ImagePickerAsset, 241 - dispatch: (action: Action) => void, 232 + dispatch: (action: VideoAction) => void, 242 233 agent: BskyAgent, 243 234 did: string, 244 235 signal: AbortSignal, 245 236 _: I18n['_'], 246 237 ) { 247 - dispatch({ 248 - type: 'idle_to_compressing', 249 - asset, 250 - signal, 251 - }) 252 - 253 238 let video: CompressedVideo | undefined 254 239 try { 255 240 video = await compressVideo(asset, {
+29 -20
src/view/com/composer/Composer.tsx
··· 82 82 import {Gif} from '#/state/queries/tenor' 83 83 import {ThreadgateAllowUISetting} from '#/state/queries/threadgate' 84 84 import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate/util' 85 + import {NO_VIDEO, NoVideoState} from '#/state/queries/video/video' 85 86 import { 86 - createVideoState, 87 87 processVideo, 88 - State as VideoUploadState, 89 - videoReducer, 88 + VideoAction, 89 + VideoState, 90 + VideoState as VideoUploadState, 90 91 } from '#/state/queries/video/video' 91 92 import {useAgent, useSession} from '#/state/session' 92 93 import {useComposerControls} from '#/state/shell/composer' ··· 192 193 const [videoAltText, setVideoAltText] = useState('') 193 194 const [captions, setCaptions] = useState<{lang: string; file: File}[]>([]) 194 195 195 - const [videoUploadState, videoDispatch] = useReducer( 196 - videoReducer, 197 - undefined, 198 - createVideoState, 196 + // TODO: Move more state here. 197 + const [composerState, dispatch] = useReducer( 198 + composerReducer, 199 + {initImageUris}, 200 + createComposerState, 201 + ) 202 + 203 + let videoUploadState: VideoState | NoVideoState = NO_VIDEO 204 + if (composerState.embed.media?.type === 'video') { 205 + videoUploadState = composerState.embed.media.video 206 + } 207 + const videoDispatch = useCallback( 208 + (videoAction: VideoAction) => { 209 + dispatch({type: 'embed_update_video', videoAction}) 210 + }, 211 + [dispatch], 199 212 ) 200 213 201 214 const selectVideo = React.useCallback( 202 215 (asset: ImagePickerAsset) => { 216 + const abortController = new AbortController() 217 + dispatch({type: 'embed_add_video', asset, abortController}) 203 218 processVideo( 204 219 asset, 205 220 videoDispatch, 206 221 agent, 207 222 currentDid, 208 - videoUploadState.abortController.signal, 223 + abortController.signal, 209 224 _, 210 225 ) 211 226 }, 212 - [_, videoUploadState.abortController, videoDispatch, agent, currentDid], 227 + [_, videoDispatch, agent, currentDid], 213 228 ) 214 229 215 230 // Whenever we receive an initial video uri, we should immediately run compression if necessary ··· 221 236 222 237 const clearVideo = React.useCallback(() => { 223 238 videoUploadState.abortController.abort() 224 - videoDispatch({type: 'to_idle', nextController: new AbortController()}) 225 - }, [videoUploadState.abortController, videoDispatch]) 239 + dispatch({type: 'embed_remove_video'}) 240 + }, [videoUploadState.abortController, dispatch]) 226 241 227 242 const updateVideoDimensions = useCallback( 228 243 (width: number, height: number) => { ··· 233 248 signal: videoUploadState.abortController.signal, 234 249 }) 235 250 }, 236 - [videoUploadState.abortController], 251 + [videoUploadState.abortController, videoDispatch], 237 252 ) 238 253 239 254 const hasVideo = Boolean(videoUploadState.asset || videoUploadState.video) ··· 249 264 ) 250 265 const [postgate, setPostgate] = useState(createPostgateRecord({post: ''})) 251 266 252 - // TODO: Move more state here. 253 - const [composerState, dispatch] = useReducer( 254 - composerReducer, 255 - {initImageUris}, 256 - createComposerState, 257 - ) 258 267 let images = NO_IMAGES 259 268 if (composerState.embed.media?.type === 'images') { 260 269 images = composerState.embed.media.images ··· 857 866 /> 858 867 <SelectVideoBtn 859 868 onSelectVideo={selectVideo} 860 - disabled={!canSelectImages} 869 + disabled={!canSelectImages || images?.length > 0} 861 870 setError={setError} 862 871 /> 863 872 <OpenCameraBtn disabled={!canSelectImages} onAdd={onImageAdd} /> ··· 1117 1126 clearVideo, 1118 1127 }: { 1119 1128 error: string 1120 - videoUploadState: VideoUploadState 1129 + videoUploadState: VideoUploadState | NoVideoState 1121 1130 clearError: () => void 1122 1131 clearVideo: () => void 1123 1132 }) {
+74 -1
src/view/com/composer/state.ts
··· 1 + import {ImagePickerAsset} from 'expo-image-picker' 2 + 1 3 import {ComposerImage, createInitialImages} from '#/state/gallery' 4 + import { 5 + createVideoState, 6 + VideoAction, 7 + videoReducer, 8 + VideoState, 9 + } from '#/state/queries/video/video' 2 10 import {ComposerOpts} from '#/state/shell/composer' 3 11 4 12 type PostRecord = { ··· 11 19 labels: string[] 12 20 } 13 21 22 + type VideoMedia = { 23 + type: 'video' 24 + video: VideoState 25 + } 26 + 14 27 type ComposerEmbed = { 15 28 // TODO: Other record types. 16 29 record: PostRecord | undefined 17 30 // TODO: Other media types. 18 - media: ImagesMedia | undefined 31 + media: ImagesMedia | VideoMedia | undefined 19 32 } 20 33 21 34 export type ComposerState = { ··· 27 40 | {type: 'embed_add_images'; images: ComposerImage[]} 28 41 | {type: 'embed_update_image'; image: ComposerImage} 29 42 | {type: 'embed_remove_image'; image: ComposerImage} 43 + | { 44 + type: 'embed_add_video' 45 + asset: ImagePickerAsset 46 + abortController: AbortController 47 + } 48 + | {type: 'embed_remove_video'} 49 + | {type: 'embed_update_video'; videoAction: VideoAction} 30 50 31 51 const MAX_IMAGES = 4 32 52 ··· 36 56 ): ComposerState { 37 57 switch (action.type) { 38 58 case 'embed_add_images': { 59 + if (action.images.length === 0) { 60 + return state 61 + } 39 62 const prevMedia = state.embed.media 40 63 let nextMedia = prevMedia 41 64 if (!prevMedia) { ··· 104 127 } 105 128 return state 106 129 } 130 + case 'embed_add_video': { 131 + const prevMedia = state.embed.media 132 + let nextMedia = prevMedia 133 + if (!prevMedia) { 134 + nextMedia = { 135 + type: 'video', 136 + video: createVideoState(action.asset, action.abortController), 137 + } 138 + } 139 + return { 140 + ...state, 141 + embed: { 142 + ...state.embed, 143 + media: nextMedia, 144 + }, 145 + } 146 + } 147 + case 'embed_update_video': { 148 + const videoAction = action.videoAction 149 + const prevMedia = state.embed.media 150 + let nextMedia = prevMedia 151 + if (prevMedia?.type === 'video') { 152 + nextMedia = { 153 + ...prevMedia, 154 + video: videoReducer(prevMedia.video, videoAction), 155 + } 156 + } 157 + return { 158 + ...state, 159 + embed: { 160 + ...state.embed, 161 + media: nextMedia, 162 + }, 163 + } 164 + } 165 + case 'embed_remove_video': { 166 + const prevMedia = state.embed.media 167 + let nextMedia = prevMedia 168 + if (prevMedia?.type === 'video') { 169 + nextMedia = undefined 170 + } 171 + return { 172 + ...state, 173 + embed: { 174 + ...state.embed, 175 + media: nextMedia, 176 + }, 177 + } 178 + } 107 179 default: 108 180 return state 109 181 } ··· 122 194 labels: [], 123 195 } 124 196 } 197 + // TODO: initial video. 125 198 return { 126 199 embed: { 127 200 record: undefined,