[Video] Upload errors and UI improvements (#5092)
* surface errors in UI * style progress indicator * remove job status progress * rm log * fix webm extzio/stable
parent
f9d736653c
commit
0e1de19903
|
@ -29,5 +29,6 @@ export async function compressVideo(
|
||||||
)
|
)
|
||||||
|
|
||||||
const info = await getVideoMetaData(compressed)
|
const info = await getVideoMetaData(compressed)
|
||||||
return {uri: compressed, size: info.size}
|
|
||||||
|
return {uri: compressed, size: info.size, mimeType: `video/${info.extension}`}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ export async function compressVideo(
|
||||||
size: blob.size,
|
size: blob.size,
|
||||||
uri,
|
uri,
|
||||||
bytes: await blob.arrayBuffer(),
|
bytes: await blob.arrayBuffer(),
|
||||||
|
mimeType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,3 +4,10 @@ export class VideoTooLargeError extends Error {
|
||||||
this.name = 'VideoTooLargeError'
|
this.name = 'VideoTooLargeError'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ServerError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'ServerError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export type CompressedVideo = {
|
export type CompressedVideo = {
|
||||||
uri: string
|
uri: string
|
||||||
|
mimeType: string
|
||||||
size: number
|
size: number
|
||||||
// web only, can fall back to uri if missing
|
// web only, can fall back to uri if missing
|
||||||
bytes?: ArrayBuffer
|
bytes?: ArrayBuffer
|
||||||
|
|
|
@ -24,3 +24,16 @@ export function useVideoAgent() {
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mimeToExt(mimeType: string) {
|
||||||
|
switch (mimeType) {
|
||||||
|
case 'video/mp4':
|
||||||
|
return 'mp4'
|
||||||
|
case 'video/webm':
|
||||||
|
return 'webm'
|
||||||
|
case 'video/mpeg':
|
||||||
|
return 'mpeg'
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported mime type: ${mimeType}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import {createUploadTask, FileSystemUploadType} from 'expo-file-system'
|
import {createUploadTask, FileSystemUploadType} from 'expo-file-system'
|
||||||
import {AppBskyVideoDefs} from '@atproto/api'
|
import {AppBskyVideoDefs} from '@atproto/api'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useMutation} from '@tanstack/react-query'
|
import {useMutation} from '@tanstack/react-query'
|
||||||
import {nanoid} from 'nanoid/non-secure'
|
import {nanoid} from 'nanoid/non-secure'
|
||||||
|
|
||||||
import {cancelable} from '#/lib/async/cancelable'
|
import {cancelable} from '#/lib/async/cancelable'
|
||||||
|
import {ServerError} from '#/lib/media/video/errors'
|
||||||
import {CompressedVideo} from '#/lib/media/video/types'
|
import {CompressedVideo} from '#/lib/media/video/types'
|
||||||
import {createVideoEndpointUrl} from '#/state/queries/video/util'
|
import {createVideoEndpointUrl, mimeToExt} from '#/state/queries/video/util'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import {getServiceAuthAudFromUrl} from 'lib/strings/url-helpers'
|
import {getServiceAuthAudFromUrl} from 'lib/strings/url-helpers'
|
||||||
|
|
||||||
|
@ -22,13 +25,14 @@ export const useUploadVideoMutation = ({
|
||||||
}) => {
|
}) => {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ['video', 'upload'],
|
mutationKey: ['video', 'upload'],
|
||||||
mutationFn: cancelable(async (video: CompressedVideo) => {
|
mutationFn: cancelable(async (video: CompressedVideo) => {
|
||||||
const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', {
|
const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', {
|
||||||
did: currentAccount!.did,
|
did: currentAccount!.did,
|
||||||
name: `${nanoid(12)}.mp4`,
|
name: `${nanoid(12)}.${mimeToExt(video.mimeType)}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
const serviceAuthAud = getServiceAuthAudFromUrl(agent.dispatchUrl)
|
const serviceAuthAud = getServiceAuthAudFromUrl(agent.dispatchUrl)
|
||||||
|
@ -50,7 +54,7 @@ export const useUploadVideoMutation = ({
|
||||||
video.uri,
|
video.uri,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'video/mp4',
|
'content-type': video.mimeType,
|
||||||
Authorization: `Bearer ${serviceAuth.token}`,
|
Authorization: `Bearer ${serviceAuth.token}`,
|
||||||
},
|
},
|
||||||
httpMethod: 'POST',
|
httpMethod: 'POST',
|
||||||
|
@ -65,6 +69,13 @@ export const useUploadVideoMutation = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseBody = JSON.parse(res.body) as AppBskyVideoDefs.JobStatus
|
const responseBody = JSON.parse(res.body) as AppBskyVideoDefs.JobStatus
|
||||||
|
|
||||||
|
if (!responseBody.jobId) {
|
||||||
|
throw new ServerError(
|
||||||
|
responseBody.error || _(msg`Failed to upload video`),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return responseBody
|
return responseBody
|
||||||
}, signal),
|
}, signal),
|
||||||
onError,
|
onError,
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import {AppBskyVideoDefs} from '@atproto/api'
|
import {AppBskyVideoDefs} from '@atproto/api'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useMutation} from '@tanstack/react-query'
|
import {useMutation} from '@tanstack/react-query'
|
||||||
import {nanoid} from 'nanoid/non-secure'
|
import {nanoid} from 'nanoid/non-secure'
|
||||||
|
|
||||||
import {cancelable} from '#/lib/async/cancelable'
|
import {cancelable} from '#/lib/async/cancelable'
|
||||||
|
import {ServerError} from '#/lib/media/video/errors'
|
||||||
import {CompressedVideo} from '#/lib/media/video/types'
|
import {CompressedVideo} from '#/lib/media/video/types'
|
||||||
import {createVideoEndpointUrl} from '#/state/queries/video/util'
|
import {createVideoEndpointUrl, mimeToExt} from '#/state/queries/video/util'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import {getServiceAuthAudFromUrl} from 'lib/strings/url-helpers'
|
import {getServiceAuthAudFromUrl} from 'lib/strings/url-helpers'
|
||||||
|
|
||||||
|
@ -21,13 +24,14 @@ export const useUploadVideoMutation = ({
|
||||||
}) => {
|
}) => {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ['video', 'upload'],
|
mutationKey: ['video', 'upload'],
|
||||||
mutationFn: cancelable(async (video: CompressedVideo) => {
|
mutationFn: cancelable(async (video: CompressedVideo) => {
|
||||||
const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', {
|
const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', {
|
||||||
did: currentAccount!.did,
|
did: currentAccount!.did,
|
||||||
name: `${nanoid(12)}.mp4`, // @TODO: make sure it's always mp4'
|
name: `${nanoid(12)}.${mimeToExt(video.mimeType)}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
const serviceAuthAud = getServiceAuthAudFromUrl(agent.dispatchUrl)
|
const serviceAuthAud = getServiceAuthAudFromUrl(agent.dispatchUrl)
|
||||||
|
@ -63,23 +67,24 @@ export const useUploadVideoMutation = ({
|
||||||
xhr.responseText,
|
xhr.responseText,
|
||||||
) as AppBskyVideoDefs.JobStatus
|
) as AppBskyVideoDefs.JobStatus
|
||||||
resolve(uploadRes)
|
resolve(uploadRes)
|
||||||
onSuccess(uploadRes)
|
|
||||||
} else {
|
} else {
|
||||||
reject()
|
reject(new ServerError(_(msg`Failed to upload video`)))
|
||||||
onError(new Error('Failed to upload video'))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
xhr.onerror = () => {
|
xhr.onerror = () => {
|
||||||
reject()
|
reject(new ServerError(_(msg`Failed to upload video`)))
|
||||||
onError(new Error('Failed to upload video'))
|
|
||||||
}
|
}
|
||||||
xhr.open('POST', uri)
|
xhr.open('POST', uri)
|
||||||
xhr.setRequestHeader('Content-Type', 'video/mp4')
|
xhr.setRequestHeader('Content-Type', video.mimeType)
|
||||||
xhr.setRequestHeader('Authorization', `Bearer ${serviceAuth.token}`)
|
xhr.setRequestHeader('Authorization', `Bearer ${serviceAuth.token}`)
|
||||||
xhr.send(bytes)
|
xhr.send(bytes)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!res.jobId) {
|
||||||
|
throw new ServerError(res.error || _(msg`Failed to upload video`))
|
||||||
|
}
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}, signal),
|
}, signal),
|
||||||
onError,
|
onError,
|
||||||
|
|
|
@ -6,7 +6,8 @@ import {useLingui} from '@lingui/react'
|
||||||
import {QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
|
import {QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {VideoTooLargeError} from 'lib/media/video/errors'
|
import {isWeb} from '#/platform/detection'
|
||||||
|
import {ServerError, VideoTooLargeError} from 'lib/media/video/errors'
|
||||||
import {CompressedVideo} from 'lib/media/video/types'
|
import {CompressedVideo} from 'lib/media/video/types'
|
||||||
import {useCompressVideoMutation} from 'state/queries/video/compress-video'
|
import {useCompressVideoMutation} from 'state/queries/video/compress-video'
|
||||||
import {useVideoAgent} from 'state/queries/video/util'
|
import {useVideoAgent} from 'state/queries/video/util'
|
||||||
|
@ -58,7 +59,12 @@ function reducer(queryClient: QueryClient) {
|
||||||
abortController: new AbortController(),
|
abortController: new AbortController(),
|
||||||
}
|
}
|
||||||
} else if (action.type === 'SetAsset') {
|
} else if (action.type === 'SetAsset') {
|
||||||
updatedState = {...state, asset: action.asset}
|
updatedState = {
|
||||||
|
...state,
|
||||||
|
asset: action.asset,
|
||||||
|
status: 'compressing',
|
||||||
|
error: undefined,
|
||||||
|
}
|
||||||
} else if (action.type === 'SetDimensions') {
|
} else if (action.type === 'SetDimensions') {
|
||||||
updatedState = {
|
updatedState = {
|
||||||
...state,
|
...state,
|
||||||
|
@ -67,11 +73,11 @@ function reducer(queryClient: QueryClient) {
|
||||||
: undefined,
|
: undefined,
|
||||||
}
|
}
|
||||||
} else if (action.type === 'SetVideo') {
|
} else if (action.type === 'SetVideo') {
|
||||||
updatedState = {...state, video: action.video}
|
updatedState = {...state, video: action.video, status: 'uploading'}
|
||||||
} else if (action.type === 'SetJobStatus') {
|
} else if (action.type === 'SetJobStatus') {
|
||||||
updatedState = {...state, jobStatus: action.jobStatus}
|
updatedState = {...state, jobStatus: action.jobStatus}
|
||||||
} else if (action.type === 'SetBlobRef') {
|
} else if (action.type === 'SetBlobRef') {
|
||||||
updatedState = {...state, blobRef: action.blobRef}
|
updatedState = {...state, blobRef: action.blobRef, status: 'done'}
|
||||||
}
|
}
|
||||||
return updatedState
|
return updatedState
|
||||||
}
|
}
|
||||||
|
@ -108,10 +114,6 @@ export function useUploadVideo({
|
||||||
type: 'SetBlobRef',
|
type: 'SetBlobRef',
|
||||||
blobRef,
|
blobRef,
|
||||||
})
|
})
|
||||||
dispatch({
|
|
||||||
type: 'SetStatus',
|
|
||||||
status: 'idle',
|
|
||||||
})
|
|
||||||
onSuccess()
|
onSuccess()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -125,10 +127,17 @@ export function useUploadVideo({
|
||||||
setJobId(response.jobId)
|
setJobId(response.jobId)
|
||||||
},
|
},
|
||||||
onError: e => {
|
onError: e => {
|
||||||
dispatch({
|
if (e instanceof ServerError) {
|
||||||
type: 'SetError',
|
dispatch({
|
||||||
error: _(msg`An error occurred while uploading the video.`),
|
type: 'SetError',
|
||||||
})
|
error: e.message,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: 'SetError',
|
||||||
|
error: _(msg`An error occurred while uploading the video.`),
|
||||||
|
})
|
||||||
|
}
|
||||||
logger.error('Error uploading video', {safeMessage: e})
|
logger.error('Error uploading video', {safeMessage: e})
|
||||||
},
|
},
|
||||||
setProgress: p => {
|
setProgress: p => {
|
||||||
|
@ -141,6 +150,13 @@ export function useUploadVideo({
|
||||||
onProgress: p => {
|
onProgress: p => {
|
||||||
dispatch({type: 'SetProgress', progress: p})
|
dispatch({type: 'SetProgress', progress: p})
|
||||||
},
|
},
|
||||||
|
onSuccess: (video: CompressedVideo) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'SetVideo',
|
||||||
|
video,
|
||||||
|
})
|
||||||
|
onVideoCompressed(video)
|
||||||
|
},
|
||||||
onError: e => {
|
onError: e => {
|
||||||
if (e instanceof VideoTooLargeError) {
|
if (e instanceof VideoTooLargeError) {
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -150,36 +166,28 @@ export function useUploadVideo({
|
||||||
} else {
|
} else {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SetError',
|
type: 'SetError',
|
||||||
// @TODO better error message from server, left untranslated on purpose
|
error: _(msg`An error occurred while compressing the video.`),
|
||||||
error: 'An error occurred while compressing the video.',
|
|
||||||
})
|
})
|
||||||
logger.error('Error compressing video', {safeMessage: e})
|
logger.error('Error compressing video', {safeMessage: e})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: (video: CompressedVideo) => {
|
|
||||||
dispatch({
|
|
||||||
type: 'SetVideo',
|
|
||||||
video,
|
|
||||||
})
|
|
||||||
dispatch({
|
|
||||||
type: 'SetStatus',
|
|
||||||
status: 'uploading',
|
|
||||||
})
|
|
||||||
onVideoCompressed(video)
|
|
||||||
},
|
|
||||||
signal: state.abortController.signal,
|
signal: state.abortController.signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectVideo = (asset: ImagePickerAsset) => {
|
const selectVideo = (asset: ImagePickerAsset) => {
|
||||||
dispatch({
|
switch (getMimeType(asset)) {
|
||||||
type: 'SetAsset',
|
case 'video/mp4':
|
||||||
asset,
|
case 'video/mpeg':
|
||||||
})
|
case 'video/webm':
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SetStatus',
|
type: 'SetAsset',
|
||||||
status: 'compressing',
|
asset,
|
||||||
})
|
})
|
||||||
onSelectVideo(asset)
|
onSelectVideo(asset)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error(_(msg`Unsupported video type: ${getMimeType(asset)}`))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearVideo = () => {
|
const clearVideo = () => {
|
||||||
|
@ -241,6 +249,21 @@ const useUploadStatusQuery = ({
|
||||||
isError,
|
isError,
|
||||||
setJobId: (_jobId: string) => {
|
setJobId: (_jobId: string) => {
|
||||||
setJobId(_jobId)
|
setJobId(_jobId)
|
||||||
|
setEnabled(true)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMimeType(asset: ImagePickerAsset) {
|
||||||
|
if (isWeb) {
|
||||||
|
const [mimeType] = asset.uri.slice('data:'.length).split(';base64,')
|
||||||
|
if (!mimeType) {
|
||||||
|
throw new Error('Could not determine mime type')
|
||||||
|
}
|
||||||
|
return mimeType
|
||||||
|
}
|
||||||
|
if (!asset.mimeType) {
|
||||||
|
throw new Error('Could not determine mime type')
|
||||||
|
}
|
||||||
|
return asset.mimeType
|
||||||
|
}
|
||||||
|
|
|
@ -181,6 +181,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
clearVideo,
|
clearVideo,
|
||||||
state: videoUploadState,
|
state: videoUploadState,
|
||||||
updateVideoDimensions,
|
updateVideoDimensions,
|
||||||
|
dispatch: videoUploadDispatch,
|
||||||
} = useUploadVideo({
|
} = useUploadVideo({
|
||||||
setStatus: setProcessingState,
|
setStatus: setProcessingState,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
@ -313,8 +314,8 @@ export const ComposePost = observer(function ComposePost({
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!finishedUploading &&
|
!finishedUploading &&
|
||||||
videoUploadState.status !== 'idle' &&
|
videoUploadState.asset &&
|
||||||
videoUploadState.asset
|
videoUploadState.status !== 'done'
|
||||||
) {
|
) {
|
||||||
setPublishOnUpload(true)
|
setPublishOnUpload(true)
|
||||||
return
|
return
|
||||||
|
@ -607,7 +608,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{error !== '' && (
|
{(error !== '' || videoUploadState.error) && (
|
||||||
<View style={[a.px_lg, a.pb_sm]}>
|
<View style={[a.px_lg, a.pb_sm]}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
@ -623,7 +624,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
]}>
|
]}>
|
||||||
<CircleInfo fill={t.palette.negative_400} />
|
<CircleInfo fill={t.palette.negative_400} />
|
||||||
<NewText style={[a.flex_1, a.leading_snug, {paddingTop: 1}]}>
|
<NewText style={[a.flex_1, a.leading_snug, {paddingTop: 1}]}>
|
||||||
{error}
|
{error || videoUploadState.error}
|
||||||
</NewText>
|
</NewText>
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Dismiss error`)}
|
label={_(msg`Dismiss error`)}
|
||||||
|
@ -638,7 +639,10 @@ export const ComposePost = observer(function ComposePost({
|
||||||
right: a.px_md.paddingRight,
|
right: a.px_md.paddingRight,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onPress={() => setError('')}>
|
onPress={() => {
|
||||||
|
if (error) setError('')
|
||||||
|
else videoUploadDispatch({type: 'Reset'})
|
||||||
|
}}>
|
||||||
<ButtonIcon icon={X} />
|
<ButtonIcon icon={X} />
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
@ -755,7 +759,8 @@ export const ComposePost = observer(function ComposePost({
|
||||||
t.atoms.border_contrast_medium,
|
t.atoms.border_contrast_medium,
|
||||||
styles.bottomBar,
|
styles.bottomBar,
|
||||||
]}>
|
]}>
|
||||||
{videoUploadState.status !== 'idle' ? (
|
{videoUploadState.status !== 'idle' &&
|
||||||
|
videoUploadState.status !== 'done' ? (
|
||||||
<VideoUploadToolbar state={videoUploadState} />
|
<VideoUploadToolbar state={videoUploadState} />
|
||||||
) : (
|
) : (
|
||||||
<ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
|
<ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||||
|
@ -764,6 +769,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
<SelectVideoBtn
|
<SelectVideoBtn
|
||||||
onSelectVideo={selectVideo}
|
onSelectVideo={selectVideo}
|
||||||
disabled={!canSelectImages}
|
disabled={!canSelectImages}
|
||||||
|
setError={setError}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} />
|
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} />
|
||||||
|
@ -1032,15 +1038,33 @@ function ToolbarWrapper({
|
||||||
|
|
||||||
function VideoUploadToolbar({state}: {state: VideoUploadState}) {
|
function VideoUploadToolbar({state}: {state: VideoUploadState}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
let text = ''
|
||||||
|
|
||||||
|
switch (state.status) {
|
||||||
|
case 'compressing':
|
||||||
|
text = _('Compressing video...')
|
||||||
|
break
|
||||||
|
case 'uploading':
|
||||||
|
text = _('Uploading video...')
|
||||||
|
break
|
||||||
|
case 'processing':
|
||||||
|
text = _('Processing video...')
|
||||||
|
break
|
||||||
|
case 'done':
|
||||||
|
text = _('Video uploaded')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// we could use state.jobStatus?.progress but 99% of the time it jumps from 0 to 100
|
||||||
const progress =
|
const progress =
|
||||||
state.status === 'compressing' || state.status === 'uploading'
|
state.status === 'compressing' || state.status === 'uploading'
|
||||||
? state.progress
|
? state.progress
|
||||||
: state.jobStatus?.progress ?? 100
|
: 100
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolbarWrapper
|
<ToolbarWrapper style={[a.flex_row, a.align_center, {paddingVertical: 5}]}>
|
||||||
style={[a.gap_sm, a.flex_row, a.align_center, {paddingVertical: 5}]}>
|
|
||||||
<ProgressCircle
|
<ProgressCircle
|
||||||
size={30}
|
size={30}
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
|
@ -1048,7 +1072,7 @@ function VideoUploadToolbar({state}: {state: VideoUploadState}) {
|
||||||
color={t.palette.primary_500}
|
color={t.palette.primary_500}
|
||||||
progress={progress}
|
progress={progress}
|
||||||
/>
|
/>
|
||||||
<Text>{state.status}</Text>
|
<NewText style={[a.font_bold, a.ml_sm]}>{text}</NewText>
|
||||||
</ToolbarWrapper>
|
</ToolbarWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,9 +19,10 @@ const VIDEO_MAX_DURATION = 90
|
||||||
type Props = {
|
type Props = {
|
||||||
onSelectVideo: (video: ImagePickerAsset) => void
|
onSelectVideo: (video: ImagePickerAsset) => void
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
setError: (error: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectVideoBtn({onSelectVideo, disabled}: Props) {
|
export function SelectVideoBtn({onSelectVideo, disabled, setError}: Props) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {requestVideoAccessIfNeeded} = useVideoLibraryPermission()
|
const {requestVideoAccessIfNeeded} = useVideoLibraryPermission()
|
||||||
|
@ -41,9 +42,17 @@ export function SelectVideoBtn({onSelectVideo, disabled}: Props) {
|
||||||
UIImagePickerPreferredAssetRepresentationMode.Current,
|
UIImagePickerPreferredAssetRepresentationMode.Current,
|
||||||
})
|
})
|
||||||
if (response.assets && response.assets.length > 0) {
|
if (response.assets && response.assets.length > 0) {
|
||||||
onSelectVideo(response.assets[0])
|
try {
|
||||||
|
onSelectVideo(response.assets[0])
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
setError(err.message)
|
||||||
|
} else {
|
||||||
|
setError(_(msg`An error occurred while selecting the video`))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [onSelectVideo, requestVideoAccessIfNeeded])
|
}, [onSelectVideo, requestVideoAccessIfNeeded, setError, _])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -59,7 +59,7 @@ export function VideoPreview({
|
||||||
<video
|
<video
|
||||||
ref={ref}
|
ref={ref}
|
||||||
src={video.uri}
|
src={video.uri}
|
||||||
style={a.flex_1}
|
style={{width: '100%', height: '100%', objectFit: 'cover'}}
|
||||||
autoPlay
|
autoPlay
|
||||||
loop
|
loop
|
||||||
muted
|
muted
|
||||||
|
|
Loading…
Reference in New Issue