[Video] Error banner improvements (#5163)

zio/stable
Samuel Newman 2024-09-06 05:25:56 +01:00 committed by GitHub
parent 18133483fe
commit 55468595d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 137 additions and 85 deletions

View File

@ -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.`),

View File

@ -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,