[Video] Error banner improvements (#5163)
parent
18133483fe
commit
55468595d0
|
@ -5,6 +5,7 @@ import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
|
import {QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {AbortError} from '#/lib/async/cancelable'
|
||||||
import {SUPPORTED_MIME_TYPES, SupportedMimeTypes} from '#/lib/constants'
|
import {SUPPORTED_MIME_TYPES, SupportedMimeTypes} from '#/lib/constants'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
|
@ -39,6 +40,8 @@ export interface State {
|
||||||
pendingPublish?: {blobRef: BlobRef; mutableProcessed: boolean}
|
pendingPublish?: {blobRef: BlobRef; mutableProcessed: boolean}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type VideoUploadDispatch = (action: Action) => void
|
||||||
|
|
||||||
function reducer(queryClient: QueryClient) {
|
function reducer(queryClient: QueryClient) {
|
||||||
return (state: State, action: Action): State => {
|
return (state: State, action: Action): State => {
|
||||||
let updatedState = state
|
let updatedState = state
|
||||||
|
@ -144,8 +147,9 @@ export function useUploadVideo({
|
||||||
setJobId(response.jobId)
|
setJobId(response.jobId)
|
||||||
},
|
},
|
||||||
onError: e => {
|
onError: e => {
|
||||||
logger.error('Error uploading video', {safeMessage: e})
|
if (e instanceof AbortError) {
|
||||||
if (e instanceof ServerError) {
|
return
|
||||||
|
} else if (e instanceof ServerError) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SetError',
|
type: 'SetError',
|
||||||
error: e.message,
|
error: e.message,
|
||||||
|
@ -176,8 +180,9 @@ export function useUploadVideo({
|
||||||
onVideoCompressed(video)
|
onVideoCompressed(video)
|
||||||
},
|
},
|
||||||
onError: e => {
|
onError: e => {
|
||||||
logger.error('Error uploading video', {safeMessage: e})
|
if (e instanceof AbortError) {
|
||||||
if (e instanceof VideoTooLargeError) {
|
return
|
||||||
|
} else if (e instanceof VideoTooLargeError) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SetError',
|
type: 'SetError',
|
||||||
error: _(msg`The selected video is larger than 100MB.`),
|
error: _(msg`The selected video is larger than 100MB.`),
|
||||||
|
|
|
@ -25,6 +25,7 @@ import Animated, {
|
||||||
FadeOut,
|
FadeOut,
|
||||||
interpolateColor,
|
interpolateColor,
|
||||||
LayoutAnimationConfig,
|
LayoutAnimationConfig,
|
||||||
|
LinearTransition,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useDerivedValue,
|
useDerivedValue,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
|
@ -45,18 +46,31 @@ import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
|
|
||||||
|
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||||
|
import * as apilib from '#/lib/api/index'
|
||||||
import {until} from '#/lib/async/until'
|
import {until} from '#/lib/async/until'
|
||||||
|
import {MAX_GRAPHEME_LENGTH} from '#/lib/constants'
|
||||||
import {
|
import {
|
||||||
createGIFDescription,
|
createGIFDescription,
|
||||||
parseAltFromGIFDescription,
|
parseAltFromGIFDescription,
|
||||||
} from '#/lib/gif-alt-text'
|
} from '#/lib/gif-alt-text'
|
||||||
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
||||||
|
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
|
||||||
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import {LikelyType} from '#/lib/link-meta/link-meta'
|
import {LikelyType} from '#/lib/link-meta/link-meta'
|
||||||
import {logEvent, useGate} from '#/lib/statsig/statsig'
|
import {logEvent, useGate} from '#/lib/statsig/statsig'
|
||||||
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
|
import {insertMentionAt} from '#/lib/strings/mention-manip'
|
||||||
|
import {shortenLinks} from '#/lib/strings/rich-text-manip'
|
||||||
|
import {colors, s} from '#/lib/styles'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
|
||||||
|
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||||
import {emitPostCreated} from '#/state/events'
|
import {emitPostCreated} from '#/state/events'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {useModals} from '#/state/modals'
|
import {useModals} from '#/state/modals'
|
||||||
|
import {GalleryModel} from '#/state/models/media/gallery'
|
||||||
import {useRequireAltTextEnabled} from '#/state/preferences'
|
import {useRequireAltTextEnabled} from '#/state/preferences'
|
||||||
import {
|
import {
|
||||||
toPostLanguages,
|
toPostLanguages,
|
||||||
|
@ -68,25 +82,38 @@ import {useProfileQuery} from '#/state/queries/profile'
|
||||||
import {Gif} from '#/state/queries/tenor'
|
import {Gif} from '#/state/queries/tenor'
|
||||||
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
|
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
|
||||||
import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate/util'
|
import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate/util'
|
||||||
import {useUploadVideo} from '#/state/queries/video/video'
|
import {
|
||||||
|
State as VideoUploadState,
|
||||||
|
useUploadVideo,
|
||||||
|
VideoUploadDispatch,
|
||||||
|
} from '#/state/queries/video/video'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import {useComposerControls} from '#/state/shell/composer'
|
import {useComposerControls} from '#/state/shell/composer'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {ComposerOpts} from '#/state/shell/composer'
|
||||||
import * as apilib from 'lib/api/index'
|
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
||||||
import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
|
import {ComposerReplyTo} from '#/view/com/composer/ComposerReplyTo'
|
||||||
import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
|
import {ExternalEmbed} from '#/view/com/composer/ExternalEmbed'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {GifAltText} from '#/view/com/composer/GifAltText'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {LabelsBtn} from '#/view/com/composer/labels/LabelsBtn'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {Gallery} from '#/view/com/composer/photos/Gallery'
|
||||||
import {insertMentionAt} from 'lib/strings/mention-manip'
|
import {OpenCameraBtn} from '#/view/com/composer/photos/OpenCameraBtn'
|
||||||
import {shortenLinks} from 'lib/strings/rich-text-manip'
|
import {SelectGifBtn} from '#/view/com/composer/photos/SelectGifBtn'
|
||||||
import {colors, s} from 'lib/styles'
|
import {SelectPhotoBtn} from '#/view/com/composer/photos/SelectPhotoBtn'
|
||||||
import {isAndroid, isIOS, isNative, isWeb} from 'platform/detection'
|
import {SelectLangBtn} from '#/view/com/composer/select-language/SelectLangBtn'
|
||||||
import {useDialogStateControlContext} from 'state/dialogs'
|
import {SuggestedLanguage} from '#/view/com/composer/select-language/SuggestedLanguage'
|
||||||
import {GalleryModel} from 'state/models/media/gallery'
|
// TODO: Prevent naming components that coincide with RN primitives
|
||||||
import {State as VideoUploadState} from 'state/queries/video/video'
|
// due to linting false positives
|
||||||
import {ComposerOpts} from 'state/shell/composer'
|
import {TextInput, TextInputRef} from '#/view/com/composer/text-input/TextInput'
|
||||||
import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo'
|
import {ThreadgateBtn} from '#/view/com/composer/threadgate/ThreadgateBtn'
|
||||||
|
import {useExternalLinkFetch} from '#/view/com/composer/useExternalLinkFetch'
|
||||||
|
import {SelectVideoBtn} from '#/view/com/composer/videos/SelectVideoBtn'
|
||||||
|
import {SubtitleDialogBtn} from '#/view/com/composer/videos/SubtitleDialog'
|
||||||
|
import {VideoPreview} from '#/view/com/composer/videos/VideoPreview'
|
||||||
|
import {VideoTranscodeProgress} from '#/view/com/composer/videos/VideoTranscodeProgress'
|
||||||
|
import {QuoteEmbed, QuoteX} from '#/view/com/util/post-embeds/QuoteEmbed'
|
||||||
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, native, useTheme} from '#/alf'
|
import {atoms as a, native, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||||
|
@ -94,29 +121,6 @@ import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {Text as NewText} from '#/components/Typography'
|
import {Text as NewText} from '#/components/Typography'
|
||||||
import {QuoteEmbed, QuoteX} from '../util/post-embeds/QuoteEmbed'
|
|
||||||
import {Text} from '../util/text/Text'
|
|
||||||
import * as Toast from '../util/Toast'
|
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
|
||||||
import {CharProgress} from './char-progress/CharProgress'
|
|
||||||
import {ExternalEmbed} from './ExternalEmbed'
|
|
||||||
import {GifAltText} from './GifAltText'
|
|
||||||
import {LabelsBtn} from './labels/LabelsBtn'
|
|
||||||
import {Gallery} from './photos/Gallery'
|
|
||||||
import {OpenCameraBtn} from './photos/OpenCameraBtn'
|
|
||||||
import {SelectGifBtn} from './photos/SelectGifBtn'
|
|
||||||
import {SelectPhotoBtn} from './photos/SelectPhotoBtn'
|
|
||||||
import {SelectLangBtn} from './select-language/SelectLangBtn'
|
|
||||||
import {SuggestedLanguage} from './select-language/SuggestedLanguage'
|
|
||||||
// TODO: Prevent naming components that coincide with RN primitives
|
|
||||||
// due to linting false positives
|
|
||||||
import {TextInput, TextInputRef} from './text-input/TextInput'
|
|
||||||
import {ThreadgateBtn} from './threadgate/ThreadgateBtn'
|
|
||||||
import {useExternalLinkFetch} from './useExternalLinkFetch'
|
|
||||||
import {SelectVideoBtn} from './videos/SelectVideoBtn'
|
|
||||||
import {SubtitleDialogBtn} from './videos/SubtitleDialog'
|
|
||||||
import {VideoPreview} from './videos/VideoPreview'
|
|
||||||
import {VideoTranscodeProgress} from './videos/VideoTranscodeProgress'
|
|
||||||
|
|
||||||
type CancelRef = {
|
type CancelRef = {
|
||||||
onPressCancel: () => void
|
onPressCancel: () => void
|
||||||
|
@ -578,7 +582,9 @@ export const ComposePost = observer(function ComposePost({
|
||||||
keyboardVerticalOffset={keyboardVerticalOffset}
|
keyboardVerticalOffset={keyboardVerticalOffset}
|
||||||
style={a.flex_1}>
|
style={a.flex_1}>
|
||||||
<View style={[a.flex_1, viewStyles]} aria-modal accessibilityViewIsModal>
|
<View style={[a.flex_1, viewStyles]} aria-modal accessibilityViewIsModal>
|
||||||
<Animated.View style={topBarAnimatedStyle}>
|
<Animated.View
|
||||||
|
style={topBarAnimatedStyle}
|
||||||
|
layout={native(LinearTransition)}>
|
||||||
<View style={styles.topbarInner}>
|
<View style={styles.topbarInner}>
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Cancel`)}
|
label={_(msg`Cancel`)}
|
||||||
|
@ -662,48 +668,15 @@ export const ComposePost = observer(function ComposePost({
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{(error !== '' || videoUploadState.error) && (
|
<ErrorBanner
|
||||||
<View style={[a.px_lg, a.pb_sm]}>
|
error={error}
|
||||||
<View
|
videoUploadState={videoUploadState}
|
||||||
style={[
|
clearError={() => setError('')}
|
||||||
a.px_md,
|
videoUploadDispatch={videoUploadDispatch}
|
||||||
a.py_sm,
|
/>
|
||||||
a.rounded_sm,
|
|
||||||
a.flex_row,
|
|
||||||
a.gap_sm,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
{
|
|
||||||
paddingRight: 48,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<CircleInfo fill={t.palette.negative_400} />
|
|
||||||
<NewText style={[a.flex_1, a.leading_snug, {paddingTop: 1}]}>
|
|
||||||
{error || videoUploadState.error}
|
|
||||||
</NewText>
|
|
||||||
<Button
|
|
||||||
label={_(msg`Dismiss error`)}
|
|
||||||
size="tiny"
|
|
||||||
color="secondary"
|
|
||||||
variant="ghost"
|
|
||||||
shape="round"
|
|
||||||
style={[
|
|
||||||
a.absolute,
|
|
||||||
{
|
|
||||||
top: a.py_sm.paddingTop,
|
|
||||||
right: a.px_md.paddingRight,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onPress={() => {
|
|
||||||
if (error) setError('')
|
|
||||||
else videoUploadDispatch({type: 'Reset'})
|
|
||||||
}}>
|
|
||||||
<ButtonIcon icon={X} />
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
<Animated.ScrollView
|
<Animated.ScrollView
|
||||||
|
layout={native(LinearTransition)}
|
||||||
onScroll={scrollHandler}
|
onScroll={scrollHandler}
|
||||||
style={styles.scrollView}
|
style={styles.scrollView}
|
||||||
keyboardShouldPersistTaps="always"
|
keyboardShouldPersistTaps="always"
|
||||||
|
@ -1083,6 +1056,80 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function ErrorBanner({
|
||||||
|
error: standardError,
|
||||||
|
videoUploadState,
|
||||||
|
clearError,
|
||||||
|
videoUploadDispatch,
|
||||||
|
}: {
|
||||||
|
error: string
|
||||||
|
videoUploadState: VideoUploadState
|
||||||
|
clearError: () => void
|
||||||
|
videoUploadDispatch: VideoUploadDispatch
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
const videoError =
|
||||||
|
videoUploadState.status !== 'idle' ? videoUploadState.error : undefined
|
||||||
|
const error = standardError || videoError
|
||||||
|
|
||||||
|
const onClearError = () => {
|
||||||
|
if (standardError) {
|
||||||
|
clearError()
|
||||||
|
} else {
|
||||||
|
videoUploadDispatch({type: 'Reset'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!error) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
style={[a.px_lg, a.pb_sm]}
|
||||||
|
entering={FadeIn}
|
||||||
|
exiting={FadeOut}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.px_md,
|
||||||
|
a.py_sm,
|
||||||
|
a.gap_xs,
|
||||||
|
a.rounded_sm,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
]}>
|
||||||
|
<View style={[a.relative, a.flex_row, a.gap_sm, {paddingRight: 48}]}>
|
||||||
|
<CircleInfo fill={t.palette.negative_400} />
|
||||||
|
<NewText style={[a.flex_1, a.leading_snug, {paddingTop: 1}]}>
|
||||||
|
{error}
|
||||||
|
</NewText>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Dismiss error`)}
|
||||||
|
size="tiny"
|
||||||
|
color="secondary"
|
||||||
|
variant="ghost"
|
||||||
|
shape="round"
|
||||||
|
style={[a.absolute, {top: 0, right: 0}]}
|
||||||
|
onPress={onClearError}>
|
||||||
|
<ButtonIcon icon={X} />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
{videoError && videoUploadState.jobStatus?.jobId && (
|
||||||
|
<NewText
|
||||||
|
style={[
|
||||||
|
{paddingLeft: 28},
|
||||||
|
a.text_xs,
|
||||||
|
a.font_bold,
|
||||||
|
a.leading_snug,
|
||||||
|
t.atoms.text_contrast_low,
|
||||||
|
]}>
|
||||||
|
<Trans>Job ID: {videoUploadState.jobStatus.jobId}</Trans>
|
||||||
|
</NewText>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function ToolbarWrapper({
|
function ToolbarWrapper({
|
||||||
style,
|
style,
|
||||||
children,
|
children,
|
||||||
|
|
Loading…
Reference in New Issue