Rename some files and variables (#5587)

* Move composer reducers together

* videoUploadState -> videoState

* Inline videoDispatch
This commit is contained in:
dan
2024-10-03 14:57:48 +09:00
committed by GitHub
parent 03704e2b48
commit 2aa365b5b6
10 changed files with 64 additions and 81 deletions
+54 -66
View File
@@ -82,13 +82,6 @@ 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 {
processVideo,
VideoAction,
VideoState,
VideoState as VideoUploadState,
} from '#/state/queries/video/video'
import {useAgent, useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {ComposerOpts} from '#/state/shell/composer'
@@ -123,7 +116,8 @@ import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import * as Prompt from '#/components/Prompt'
import {Text as NewText} from '#/components/Typography'
import {composerReducer, createComposerState} from './state'
import {composerReducer, createComposerState} from './state/composer'
import {NO_VIDEO, NoVideoState, processVideo, VideoState} from './state/video'
const MAX_IMAGES = 4
@@ -200,16 +194,10 @@ export const ComposePost = ({
createComposerState,
)
let videoUploadState: VideoState | NoVideoState = NO_VIDEO
let videoState: VideoState | NoVideoState = NO_VIDEO
if (composerState.embed.media?.type === 'video') {
videoUploadState = composerState.embed.media.video
videoState = composerState.embed.media.video
}
const videoDispatch = useCallback(
(videoAction: VideoAction) => {
dispatch({type: 'embed_update_video', videoAction})
},
[dispatch],
)
const selectVideo = React.useCallback(
(asset: ImagePickerAsset) => {
@@ -217,14 +205,14 @@ export const ComposePost = ({
dispatch({type: 'embed_add_video', asset, abortController})
processVideo(
asset,
videoDispatch,
videoAction => dispatch({type: 'embed_update_video', videoAction}),
agent,
currentDid,
abortController.signal,
_,
)
},
[_, videoDispatch, agent, currentDid],
[_, agent, currentDid],
)
// Whenever we receive an initial video uri, we should immediately run compression if necessary
@@ -235,23 +223,26 @@ export const ComposePost = ({
}, [initVideoUri, selectVideo])
const clearVideo = React.useCallback(() => {
videoUploadState.abortController.abort()
videoState.abortController.abort()
dispatch({type: 'embed_remove_video'})
}, [videoUploadState.abortController, dispatch])
}, [videoState.abortController, dispatch])
const updateVideoDimensions = useCallback(
(width: number, height: number) => {
videoDispatch({
type: 'update_dimensions',
width,
height,
signal: videoUploadState.abortController.signal,
dispatch({
type: 'embed_update_video',
videoAction: {
type: 'update_dimensions',
width,
height,
signal: videoState.abortController.signal,
},
})
},
[videoUploadState.abortController, videoDispatch],
[videoState.abortController],
)
const hasVideo = Boolean(videoUploadState.asset || videoUploadState.video)
const hasVideo = Boolean(videoState.asset || videoState.video)
const [publishOnUpload, setPublishOnUpload] = useState(false)
@@ -288,7 +279,7 @@ export const ComposePost = ({
graphemeLength > 0 ||
images.length !== 0 ||
extGif ||
videoUploadState.status !== 'idle'
videoState.status !== 'idle'
) {
closeAllDialogs()
Keyboard.dismiss()
@@ -303,7 +294,7 @@ export const ComposePost = ({
closeAllDialogs,
discardPromptControl,
onClose,
videoUploadState.status,
videoState.status,
])
useImperativeHandle(cancelRef, () => ({onPressCancel}))
@@ -400,8 +391,8 @@ export const ComposePost = ({
if (
!finishedUploading &&
videoUploadState.asset &&
videoUploadState.status !== 'done'
videoState.asset &&
videoState.status !== 'done'
) {
setPublishOnUpload(true)
return
@@ -414,7 +405,7 @@ export const ComposePost = ({
images.length === 0 &&
!extLink &&
!quote &&
videoUploadState.status === 'idle'
videoState.status === 'idle'
) {
setError(_(msg`Did you want to say anything?`))
return
@@ -442,14 +433,14 @@ export const ComposePost = ({
onStateChange: setProcessingState,
langs: toPostLanguages(langPrefs.postLanguage),
video:
videoUploadState.status === 'done'
videoState.status === 'done'
? {
blobRef: videoUploadState.pendingPublish.blobRef,
blobRef: videoState.pendingPublish.blobRef,
altText: videoAltText,
captions: captions,
aspectRatio: {
width: videoUploadState.asset.width,
height: videoUploadState.asset.height,
width: videoState.asset.width,
height: videoState.asset.height,
},
}
: undefined,
@@ -550,20 +541,20 @@ export const ComposePost = ({
setLangPrefs,
threadgateAllowUISettings,
videoAltText,
videoUploadState.asset,
videoUploadState.pendingPublish,
videoUploadState.status,
videoState.asset,
videoState.pendingPublish,
videoState.status,
],
)
React.useEffect(() => {
if (videoUploadState.pendingPublish && publishOnUpload) {
if (!videoUploadState.pendingPublish.mutableProcessed) {
videoUploadState.pendingPublish.mutableProcessed = true
if (videoState.pendingPublish && publishOnUpload) {
if (!videoState.pendingPublish.mutableProcessed) {
videoState.pendingPublish.mutableProcessed = true
onPressPublish(true)
}
}
}, [onPressPublish, publishOnUpload, videoUploadState.pendingPublish])
}, [onPressPublish, publishOnUpload, videoState.pendingPublish])
const canPost = useMemo(
() => graphemeLength <= MAX_GRAPHEME_LENGTH && !isAltTextRequiredAndMissing,
@@ -576,10 +567,10 @@ export const ComposePost = ({
const canSelectImages =
images.length < MAX_IMAGES &&
!extLink &&
videoUploadState.status === 'idle' &&
!videoUploadState.video
videoState.status === 'idle' &&
!videoState.video
const hasMedia =
images.length > 0 || Boolean(extLink) || Boolean(videoUploadState.video)
images.length > 0 || Boolean(extLink) || Boolean(videoState.video)
const onEmojiButtonPress = useCallback(() => {
openEmojiPicker?.(textInput.current?.getCursorPosition())
@@ -694,9 +685,7 @@ export const ComposePost = ({
size="small"
style={[a.rounded_full, a.py_sm]}
onPress={() => onPressPublish()}
disabled={
videoUploadState.status !== 'idle' && publishOnUpload
}>
disabled={videoState.status !== 'idle' && publishOnUpload}>
<ButtonText style={[a.text_md]}>
{replyTo ? (
<Trans context="action">Reply</Trans>
@@ -732,7 +721,7 @@ export const ComposePost = ({
)}
<ErrorBanner
error={error}
videoUploadState={videoUploadState}
videoState={videoState}
clearError={() => setError('')}
clearVideo={clearVideo}
/>
@@ -798,17 +787,17 @@ export const ComposePost = ({
style={[a.w_full, a.mt_lg]}
entering={native(ZoomIn)}
exiting={native(ZoomOut)}>
{videoUploadState.asset &&
(videoUploadState.status === 'compressing' ? (
{videoState.asset &&
(videoState.status === 'compressing' ? (
<VideoTranscodeProgress
asset={videoUploadState.asset}
progress={videoUploadState.progress}
asset={videoState.asset}
progress={videoState.progress}
clear={clearVideo}
/>
) : videoUploadState.video ? (
) : videoState.video ? (
<VideoPreview
asset={videoUploadState.asset}
video={videoUploadState.video}
asset={videoState.asset}
video={videoState.video}
setDimensions={updateVideoDimensions}
clear={clearVideo}
/>
@@ -854,9 +843,8 @@ export const ComposePost = ({
t.atoms.border_contrast_medium,
styles.bottomBar,
]}>
{videoUploadState.status !== 'idle' &&
videoUploadState.status !== 'done' ? (
<VideoUploadToolbar state={videoUploadState} />
{videoState.status !== 'idle' && videoState.status !== 'done' ? (
<VideoUploadToolbar state={videoState} />
) : (
<ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
<SelectPhotoBtn
@@ -1121,12 +1109,12 @@ const styles = StyleSheet.create({
function ErrorBanner({
error: standardError,
videoUploadState,
videoState,
clearError,
clearVideo,
}: {
error: string
videoUploadState: VideoUploadState | NoVideoState
videoState: VideoState | NoVideoState
clearError: () => void
clearVideo: () => void
}) {
@@ -1134,7 +1122,7 @@ function ErrorBanner({
const {_} = useLingui()
const videoError =
videoUploadState.status === 'error' ? videoUploadState.error : undefined
videoState.status === 'error' ? videoState.error : undefined
const error = standardError || videoError
const onClearError = () => {
@@ -1176,7 +1164,7 @@ function ErrorBanner({
<ButtonIcon icon={X} />
</Button>
</View>
{videoError && videoUploadState.jobId && (
{videoError && videoState.jobId && (
<NewText
style={[
{paddingLeft: 28},
@@ -1185,7 +1173,7 @@ function ErrorBanner({
a.leading_snug,
t.atoms.text_contrast_low,
]}>
<Trans>Job ID: {videoUploadState.jobId}</Trans>
<Trans>Job ID: {videoState.jobId}</Trans>
</NewText>
)}
</View>
@@ -1211,7 +1199,7 @@ function ToolbarWrapper({
)
}
function VideoUploadToolbar({state}: {state: VideoUploadState}) {
function VideoUploadToolbar({state}: {state: VideoState}) {
const t = useTheme()
const {_} = useLingui()
const progress = state.progress
+1 -1
View File
@@ -21,7 +21,7 @@ import {ComposerImage, cropImage} from '#/state/gallery'
import {Text} from '#/view/com/util/text/Text'
import {useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {ComposerAction} from '../state'
import {ComposerAction} from '../state/composer'
import {EditImageDialog} from './EditImageDialog'
import {ImageAltTextDialog} from './ImageAltTextDialog'
@@ -1,13 +1,8 @@
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'
import {createVideoState, VideoAction, videoReducer, VideoState} from './video'
type PostRecord = {
uri: string
+406
View File
@@ -0,0 +1,406 @@
import {ImagePickerAsset} from 'expo-image-picker'
import {AppBskyVideoDefs, BlobRef, BskyAgent} from '@atproto/api'
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 {
ServerError,
UploadLimitError,
VideoTooLargeError,
} from '#/lib/media/video/errors'
import {CompressedVideo} from '#/lib/media/video/types'
import {logger} from '#/logger'
export type VideoAction =
| {
type: 'compressing_to_uploading'
video: CompressedVideo
signal: AbortSignal
}
| {
type: 'uploading_to_processing'
jobId: string
signal: AbortSignal
}
| {type: 'to_error'; error: string; signal: AbortSignal}
| {
type: 'to_done'
blobRef: BlobRef
signal: AbortSignal
}
| {type: 'update_progress'; progress: number; signal: AbortSignal}
| {
type: 'update_dimensions'
width: number
height: number
signal: AbortSignal
}
| {
type: 'update_job_status'
jobStatus: AppBskyVideoDefs.JobStatus
signal: AbortSignal
}
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'
progress: 100
abortController: AbortController
asset: ImagePickerAsset | null
video: CompressedVideo | null
jobId: string | null
error: string
pendingPublish?: undefined
}
type CompressingState = {
status: 'compressing'
progress: number
abortController: AbortController
asset: ImagePickerAsset
video?: undefined
jobId?: undefined
pendingPublish?: undefined
}
type UploadingState = {
status: 'uploading'
progress: number
abortController: AbortController
asset: ImagePickerAsset
video: CompressedVideo
jobId?: undefined
pendingPublish?: undefined
}
type ProcessingState = {
status: 'processing'
progress: number
abortController: AbortController
asset: ImagePickerAsset
video: CompressedVideo
jobId: string
jobStatus: AppBskyVideoDefs.JobStatus | null
pendingPublish?: undefined
}
type DoneState = {
status: 'done'
progress: 100
abortController: AbortController
asset: ImagePickerAsset
video: CompressedVideo
jobId?: undefined
pendingPublish: {blobRef: BlobRef; mutableProcessed: boolean}
}
export type VideoState =
| ErrorState
| CompressingState
| UploadingState
| ProcessingState
| DoneState
export function createVideoState(
asset: ImagePickerAsset,
abortController: AbortController,
): CompressingState {
return {
status: 'compressing',
progress: 0,
abortController,
asset,
}
}
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
}
if (action.type === 'to_error') {
return {
status: 'error',
progress: 100,
abortController: state.abortController,
error: action.error,
asset: state.asset ?? null,
video: state.video ?? null,
jobId: state.jobId ?? null,
}
} else if (action.type === 'update_progress') {
if (state.status === 'compressing' || state.status === 'uploading') {
return {
...state,
progress: action.progress,
}
}
} else if (action.type === 'update_dimensions') {
if (state.asset) {
return {
...state,
asset: {...state.asset, width: action.width, height: action.height},
}
}
} else if (action.type === 'compressing_to_uploading') {
if (state.status === 'compressing') {
return {
status: 'uploading',
progress: 0,
abortController: state.abortController,
asset: state.asset,
video: action.video,
}
}
return state
} else if (action.type === 'uploading_to_processing') {
if (state.status === 'uploading') {
return {
status: 'processing',
progress: 0,
abortController: state.abortController,
asset: state.asset,
video: state.video,
jobId: action.jobId,
jobStatus: null,
}
}
} else if (action.type === 'update_job_status') {
if (state.status === 'processing') {
return {
...state,
jobStatus: action.jobStatus,
progress:
action.jobStatus.progress !== undefined
? action.jobStatus.progress / 100
: state.progress,
}
}
} else if (action.type === 'to_done') {
if (state.status === 'processing') {
return {
status: 'done',
progress: 100,
abortController: state.abortController,
asset: state.asset,
video: state.video,
pendingPublish: {
blobRef: action.blobRef,
mutableProcessed: false,
},
}
}
}
console.error(
'Unexpected video action (' +
action.type +
') while in ' +
state.status +
' state',
)
return state
}
function trunc2dp(num: number) {
return Math.trunc(num * 100) / 100
}
export async function processVideo(
asset: ImagePickerAsset,
dispatch: (action: VideoAction) => void,
agent: BskyAgent,
did: string,
signal: AbortSignal,
_: I18n['_'],
) {
let video: CompressedVideo | undefined
try {
video = await compressVideo(asset, {
onProgress: num => {
dispatch({type: 'update_progress', progress: trunc2dp(num), signal})
},
signal,
})
} catch (e) {
const message = getCompressErrorMessage(e, _)
if (message !== null) {
dispatch({
type: 'to_error',
error: message,
signal,
})
}
return
}
dispatch({
type: 'compressing_to_uploading',
video,
signal,
})
let uploadResponse: AppBskyVideoDefs.JobStatus | undefined
try {
uploadResponse = await uploadVideo({
video,
agent,
did,
signal,
_,
setProgress: p => {
dispatch({type: 'update_progress', progress: p, signal})
},
})
} catch (e) {
const message = getUploadErrorMessage(e, _)
if (message !== null) {
dispatch({
type: 'to_error',
error: message,
signal,
})
}
return
}
const jobId = uploadResponse.jobId
dispatch({
type: 'uploading_to_processing',
jobId,
signal,
})
let pollFailures = 0
while (true) {
if (signal.aborted) {
return // Exit async loop
}
const videoAgent = createVideoAgent()
let status: JobStatus | undefined
let blob: BlobRef | undefined
try {
const response = await videoAgent.app.bsky.video.getJobStatus({jobId})
status = response.data.jobStatus
pollFailures = 0
if (status.state === 'JOB_STATE_COMPLETED') {
blob = status.blob
if (!blob) {
throw new Error('Job completed, but did not return a blob')
}
} else if (status.state === 'JOB_STATE_FAILED') {
throw new Error(status.error ?? 'Job failed to process')
}
} catch (e) {
if (!status) {
pollFailures++
if (pollFailures < 50) {
await new Promise(resolve => setTimeout(resolve, 5000))
continue // Continue async loop
}
}
logger.error('Error processing video', {safeMessage: e})
dispatch({
type: 'to_error',
error: _(msg`Video failed to process`),
signal,
})
return // Exit async loop
}
if (blob) {
dispatch({
type: 'to_done',
blobRef: blob,
signal,
})
} else {
dispatch({
type: 'update_job_status',
jobStatus: status,
signal,
})
}
if (
status.state !== 'JOB_STATE_COMPLETED' &&
status.state !== 'JOB_STATE_FAILED'
) {
await new Promise(resolve => setTimeout(resolve, 1500))
continue // Continue async loop
}
return // Exit async loop
}
}
function getCompressErrorMessage(e: unknown, _: I18n['_']): string | null {
if (e instanceof AbortError) {
return null
}
if (e instanceof VideoTooLargeError) {
return _(msg`The selected video is larger than 50MB.`)
}
logger.error('Error compressing video', {safeMessage: e})
return _(msg`An error occurred while compressing the video.`)
}
function getUploadErrorMessage(e: unknown, _: I18n['_']): string | null {
if (e instanceof AbortError) {
return null
}
logger.error('Error uploading video', {safeMessage: e})
if (e instanceof ServerError || e instanceof UploadLimitError) {
// https://github.com/bluesky-social/tango/blob/lumi/lumi/worker/permissions.go#L77
switch (e.message) {
case 'User is not allowed to upload videos':
return _(msg`You are not allowed to upload videos.`)
case 'Uploading is disabled at the moment':
return _(
msg`Hold up! Were gradually giving access to video, and youre still waiting in line. Check back soon!`,
)
case "Failed to get user's upload stats":
return _(
msg`We were unable to determine if you are allowed to upload videos. Please try again.`,
)
case 'User has exceeded daily upload bytes limit':
return _(
msg`You've reached your daily limit for video uploads (too many bytes)`,
)
case 'User has exceeded daily upload videos limit':
return _(
msg`You've reached your daily limit for video uploads (too many videos)`,
)
case 'Account is not old enough to upload videos':
return _(
msg`Your account is not yet old enough to upload videos. Please try again later.`,
)
default:
return e.message
}
}
return _(msg`An error occurred while uploading the video.`)
}