Rename some files and variables (#5587)
* Move composer reducers together * videoUploadState -> videoState * Inline videoDispatch
This commit is contained in:
@@ -1,53 +0,0 @@
|
||||
import {AtpAgent} from '@atproto/api'
|
||||
|
||||
import {SupportedMimeTypes, VIDEO_SERVICE} from '#/lib/constants'
|
||||
|
||||
export const createVideoEndpointUrl = (
|
||||
route: string,
|
||||
params?: Record<string, string>,
|
||||
) => {
|
||||
const url = new URL(VIDEO_SERVICE)
|
||||
url.pathname = route
|
||||
if (params) {
|
||||
for (const key in params) {
|
||||
url.searchParams.set(key, params[key])
|
||||
}
|
||||
}
|
||||
return url.href
|
||||
}
|
||||
|
||||
export function createVideoAgent() {
|
||||
return new AtpAgent({
|
||||
service: VIDEO_SERVICE,
|
||||
})
|
||||
}
|
||||
|
||||
export function mimeToExt(mimeType: SupportedMimeTypes | (string & {})) {
|
||||
switch (mimeType) {
|
||||
case 'video/mp4':
|
||||
return 'mp4'
|
||||
case 'video/webm':
|
||||
return 'webm'
|
||||
case 'video/mpeg':
|
||||
return 'mpeg'
|
||||
case 'video/quicktime':
|
||||
return 'mov'
|
||||
default:
|
||||
throw new Error(`Unsupported mime type: ${mimeType}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function extToMime(ext: string) {
|
||||
switch (ext) {
|
||||
case 'mp4':
|
||||
return 'video/mp4'
|
||||
case 'webm':
|
||||
return 'video/webm'
|
||||
case 'mpeg':
|
||||
return 'video/mpeg'
|
||||
case 'mov':
|
||||
return 'video/quicktime'
|
||||
default:
|
||||
throw new Error(`Unsupported file extension: ${ext}`)
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import {BskyAgent} from '@atproto/api'
|
||||
import {I18n} from '@lingui/core'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
import {VIDEO_SERVICE_DID} from '#/lib/constants'
|
||||
import {UploadLimitError} from '#/lib/media/video/errors'
|
||||
import {getServiceAuthAudFromUrl} from '#/lib/strings/url-helpers'
|
||||
import {createVideoAgent} from './util'
|
||||
|
||||
export async function getServiceAuthToken({
|
||||
agent,
|
||||
aud,
|
||||
lxm,
|
||||
exp,
|
||||
}: {
|
||||
agent: BskyAgent
|
||||
aud?: string
|
||||
lxm: string
|
||||
exp?: number
|
||||
}) {
|
||||
const pdsAud = getServiceAuthAudFromUrl(agent.dispatchUrl)
|
||||
if (!pdsAud) {
|
||||
throw new Error('Agent does not have a PDS URL')
|
||||
}
|
||||
const {data: serviceAuth} = await agent.com.atproto.server.getServiceAuth({
|
||||
aud: aud ?? pdsAud,
|
||||
lxm,
|
||||
exp,
|
||||
})
|
||||
return serviceAuth.token
|
||||
}
|
||||
|
||||
export async function getVideoUploadLimits(agent: BskyAgent, _: I18n['_']) {
|
||||
const token = await getServiceAuthToken({
|
||||
agent,
|
||||
lxm: 'app.bsky.video.getUploadLimits',
|
||||
aud: VIDEO_SERVICE_DID,
|
||||
})
|
||||
const videoAgent = createVideoAgent()
|
||||
const {data: limits} = await videoAgent.app.bsky.video
|
||||
.getUploadLimits({}, {headers: {Authorization: `Bearer ${token}`}})
|
||||
.catch(err => {
|
||||
if (err instanceof Error) {
|
||||
throw new UploadLimitError(err.message)
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
})
|
||||
|
||||
if (!limits.canUpload) {
|
||||
if (limits.message) {
|
||||
throw new UploadLimitError(limits.message)
|
||||
} else {
|
||||
throw new UploadLimitError(
|
||||
_(
|
||||
msg`You have temporarily reached the limit for video uploads. Please try again later.`,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import {createUploadTask, FileSystemUploadType} from 'expo-file-system'
|
||||
import {AppBskyVideoDefs, BskyAgent} from '@atproto/api'
|
||||
import {I18n} from '@lingui/core'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {AbortError} from '#/lib/async/cancelable'
|
||||
import {ServerError} from '#/lib/media/video/errors'
|
||||
import {CompressedVideo} from '#/lib/media/video/types'
|
||||
import {createVideoEndpointUrl, mimeToExt} from '#/state/queries/video/util'
|
||||
import {getServiceAuthToken, getVideoUploadLimits} from './video-upload.shared'
|
||||
|
||||
export async function uploadVideo({
|
||||
video,
|
||||
agent,
|
||||
did,
|
||||
setProgress,
|
||||
signal,
|
||||
_,
|
||||
}: {
|
||||
video: CompressedVideo
|
||||
agent: BskyAgent
|
||||
did: string
|
||||
setProgress: (progress: number) => void
|
||||
signal: AbortSignal
|
||||
_: I18n['_']
|
||||
}) {
|
||||
if (signal.aborted) {
|
||||
throw new AbortError()
|
||||
}
|
||||
await getVideoUploadLimits(agent, _)
|
||||
|
||||
const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', {
|
||||
did,
|
||||
name: `${nanoid(12)}.${mimeToExt(video.mimeType)}`,
|
||||
})
|
||||
|
||||
if (signal.aborted) {
|
||||
throw new AbortError()
|
||||
}
|
||||
const token = await getServiceAuthToken({
|
||||
agent,
|
||||
lxm: 'com.atproto.repo.uploadBlob',
|
||||
exp: Date.now() / 1000 + 60 * 30, // 30 minutes
|
||||
})
|
||||
const uploadTask = createUploadTask(
|
||||
uri,
|
||||
video.uri,
|
||||
{
|
||||
headers: {
|
||||
'content-type': video.mimeType,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
httpMethod: 'POST',
|
||||
uploadType: FileSystemUploadType.BINARY_CONTENT,
|
||||
},
|
||||
p => setProgress(p.totalBytesSent / p.totalBytesExpectedToSend),
|
||||
)
|
||||
|
||||
if (signal.aborted) {
|
||||
throw new AbortError()
|
||||
}
|
||||
const res = await uploadTask.uploadAsync()
|
||||
|
||||
if (!res?.body) {
|
||||
throw new Error('No response')
|
||||
}
|
||||
|
||||
const responseBody = JSON.parse(res.body) as AppBskyVideoDefs.JobStatus
|
||||
|
||||
if (!responseBody.jobId) {
|
||||
throw new ServerError(responseBody.error || _(msg`Failed to upload video`))
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
throw new AbortError()
|
||||
}
|
||||
return responseBody
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import {AppBskyVideoDefs} from '@atproto/api'
|
||||
import {BskyAgent} from '@atproto/api'
|
||||
import {I18n} from '@lingui/core'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {AbortError} from '#/lib/async/cancelable'
|
||||
import {ServerError} from '#/lib/media/video/errors'
|
||||
import {CompressedVideo} from '#/lib/media/video/types'
|
||||
import {createVideoEndpointUrl, mimeToExt} from '#/state/queries/video/util'
|
||||
import {getServiceAuthToken, getVideoUploadLimits} from './video-upload.shared'
|
||||
|
||||
export async function uploadVideo({
|
||||
video,
|
||||
agent,
|
||||
did,
|
||||
setProgress,
|
||||
signal,
|
||||
_,
|
||||
}: {
|
||||
video: CompressedVideo
|
||||
agent: BskyAgent
|
||||
did: string
|
||||
setProgress: (progress: number) => void
|
||||
signal: AbortSignal
|
||||
_: I18n['_']
|
||||
}) {
|
||||
if (signal.aborted) {
|
||||
throw new AbortError()
|
||||
}
|
||||
await getVideoUploadLimits(agent, _)
|
||||
|
||||
const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', {
|
||||
did,
|
||||
name: `${nanoid(12)}.${mimeToExt(video.mimeType)}`,
|
||||
})
|
||||
|
||||
let bytes = video.bytes
|
||||
if (!bytes) {
|
||||
if (signal.aborted) {
|
||||
throw new AbortError()
|
||||
}
|
||||
bytes = await fetch(video.uri).then(res => res.arrayBuffer())
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
throw new AbortError()
|
||||
}
|
||||
const token = await getServiceAuthToken({
|
||||
agent,
|
||||
lxm: 'com.atproto.repo.uploadBlob',
|
||||
exp: Date.now() / 1000 + 60 * 30, // 30 minutes
|
||||
})
|
||||
|
||||
if (signal.aborted) {
|
||||
throw new AbortError()
|
||||
}
|
||||
const xhr = new XMLHttpRequest()
|
||||
const res = await new Promise<AppBskyVideoDefs.JobStatus>(
|
||||
(resolve, reject) => {
|
||||
xhr.upload.addEventListener('progress', e => {
|
||||
const progress = e.loaded / e.total
|
||||
setProgress(progress)
|
||||
})
|
||||
xhr.onloadend = () => {
|
||||
if (signal.aborted) {
|
||||
reject(new AbortError())
|
||||
} else if (xhr.readyState === 4) {
|
||||
const uploadRes = JSON.parse(
|
||||
xhr.responseText,
|
||||
) as AppBskyVideoDefs.JobStatus
|
||||
resolve(uploadRes)
|
||||
} else {
|
||||
reject(new ServerError(_(msg`Failed to upload video`)))
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => {
|
||||
reject(new ServerError(_(msg`Failed to upload video`)))
|
||||
}
|
||||
xhr.open('POST', uri)
|
||||
xhr.setRequestHeader('Content-Type', video.mimeType)
|
||||
xhr.setRequestHeader('Authorization', `Bearer ${token}`)
|
||||
xhr.send(bytes)
|
||||
},
|
||||
)
|
||||
|
||||
if (!res.jobId) {
|
||||
throw new ServerError(res.error || _(msg`Failed to upload video`))
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
throw new AbortError()
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -1,406 +0,0 @@
|
||||
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 {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'
|
||||
import {createVideoAgent} from '#/state/queries/video/util'
|
||||
import {uploadVideo} from '#/state/queries/video/video-upload'
|
||||
|
||||
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! We’re gradually giving access to video, and you’re 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.`)
|
||||
}
|
||||
Reference in New Issue
Block a user