Bluesky app fork with some witchin' additions 💫

[Video] Error banner improvements (#5163)

authored by samuel.fm and committed by

GitHub 55468595 18133483

+137 -85
+9 -4
src/state/queries/video/video.ts
··· 5 5 import {useLingui} from '@lingui/react' 6 6 import {QueryClient, useQuery, useQueryClient} from '@tanstack/react-query' 7 7 8 + import {AbortError} from '#/lib/async/cancelable' 8 9 import {SUPPORTED_MIME_TYPES, SupportedMimeTypes} from '#/lib/constants' 9 10 import {logger} from '#/logger' 10 11 import {isWeb} from '#/platform/detection' ··· 38 39 abortController: AbortController 39 40 pendingPublish?: {blobRef: BlobRef; mutableProcessed: boolean} 40 41 } 42 + 43 + export type VideoUploadDispatch = (action: Action) => void 41 44 42 45 function reducer(queryClient: QueryClient) { 43 46 return (state: State, action: Action): State => { ··· 144 147 setJobId(response.jobId) 145 148 }, 146 149 onError: e => { 147 - logger.error('Error uploading video', {safeMessage: e}) 148 - if (e instanceof ServerError) { 150 + if (e instanceof AbortError) { 151 + return 152 + } else if (e instanceof ServerError) { 149 153 dispatch({ 150 154 type: 'SetError', 151 155 error: e.message, ··· 176 180 onVideoCompressed(video) 177 181 }, 178 182 onError: e => { 179 - logger.error('Error uploading video', {safeMessage: e}) 180 - if (e instanceof VideoTooLargeError) { 183 + if (e instanceof AbortError) { 184 + return 185 + } else if (e instanceof VideoTooLargeError) { 181 186 dispatch({ 182 187 type: 'SetError', 183 188 error: _(msg`The selected video is larger than 100MB.`),
+128 -81
src/view/com/composer/Composer.tsx
··· 25 25 FadeOut, 26 26 interpolateColor, 27 27 LayoutAnimationConfig, 28 + LinearTransition, 28 29 useAnimatedStyle, 29 30 useDerivedValue, 30 31 useSharedValue, ··· 45 46 import {useLingui} from '@lingui/react' 46 47 import {observer} from 'mobx-react-lite' 47 48 49 + import {useAnalytics} from '#/lib/analytics/analytics' 50 + import * as apilib from '#/lib/api/index' 48 51 import {until} from '#/lib/async/until' 52 + import {MAX_GRAPHEME_LENGTH} from '#/lib/constants' 49 53 import { 50 54 createGIFDescription, 51 55 parseAltFromGIFDescription, 52 56 } from '#/lib/gif-alt-text' 53 57 import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' 58 + import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible' 59 + import {usePalette} from '#/lib/hooks/usePalette' 60 + import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 54 61 import {LikelyType} from '#/lib/link-meta/link-meta' 55 62 import {logEvent, useGate} from '#/lib/statsig/statsig' 63 + import {cleanError} from '#/lib/strings/errors' 64 + import {insertMentionAt} from '#/lib/strings/mention-manip' 65 + import {shortenLinks} from '#/lib/strings/rich-text-manip' 66 + import {colors, s} from '#/lib/styles' 56 67 import {logger} from '#/logger' 68 + import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection' 69 + import {useDialogStateControlContext} from '#/state/dialogs' 57 70 import {emitPostCreated} from '#/state/events' 58 71 import {useModalControls} from '#/state/modals' 59 72 import {useModals} from '#/state/modals' 73 + import {GalleryModel} from '#/state/models/media/gallery' 60 74 import {useRequireAltTextEnabled} from '#/state/preferences' 61 75 import { 62 76 toPostLanguages, ··· 68 82 import {Gif} from '#/state/queries/tenor' 69 83 import {ThreadgateAllowUISetting} from '#/state/queries/threadgate' 70 84 import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate/util' 71 - import {useUploadVideo} from '#/state/queries/video/video' 85 + import { 86 + State as VideoUploadState, 87 + useUploadVideo, 88 + VideoUploadDispatch, 89 + } from '#/state/queries/video/video' 72 90 import {useAgent, useSession} from '#/state/session' 73 91 import {useComposerControls} from '#/state/shell/composer' 74 - import {useAnalytics} from 'lib/analytics/analytics' 75 - import * as apilib from 'lib/api/index' 76 - import {MAX_GRAPHEME_LENGTH} from 'lib/constants' 77 - import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible' 78 - import {usePalette} from 'lib/hooks/usePalette' 79 - import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 80 - import {cleanError} from 'lib/strings/errors' 81 - import {insertMentionAt} from 'lib/strings/mention-manip' 82 - import {shortenLinks} from 'lib/strings/rich-text-manip' 83 - import {colors, s} from 'lib/styles' 84 - import {isAndroid, isIOS, isNative, isWeb} from 'platform/detection' 85 - import {useDialogStateControlContext} from 'state/dialogs' 86 - import {GalleryModel} from 'state/models/media/gallery' 87 - import {State as VideoUploadState} from 'state/queries/video/video' 88 - import {ComposerOpts} from 'state/shell/composer' 89 - import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo' 92 + import {ComposerOpts} from '#/state/shell/composer' 93 + import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' 94 + import {ComposerReplyTo} from '#/view/com/composer/ComposerReplyTo' 95 + import {ExternalEmbed} from '#/view/com/composer/ExternalEmbed' 96 + import {GifAltText} from '#/view/com/composer/GifAltText' 97 + import {LabelsBtn} from '#/view/com/composer/labels/LabelsBtn' 98 + import {Gallery} from '#/view/com/composer/photos/Gallery' 99 + import {OpenCameraBtn} from '#/view/com/composer/photos/OpenCameraBtn' 100 + import {SelectGifBtn} from '#/view/com/composer/photos/SelectGifBtn' 101 + import {SelectPhotoBtn} from '#/view/com/composer/photos/SelectPhotoBtn' 102 + import {SelectLangBtn} from '#/view/com/composer/select-language/SelectLangBtn' 103 + import {SuggestedLanguage} from '#/view/com/composer/select-language/SuggestedLanguage' 104 + // TODO: Prevent naming components that coincide with RN primitives 105 + // due to linting false positives 106 + import {TextInput, TextInputRef} from '#/view/com/composer/text-input/TextInput' 107 + import {ThreadgateBtn} from '#/view/com/composer/threadgate/ThreadgateBtn' 108 + import {useExternalLinkFetch} from '#/view/com/composer/useExternalLinkFetch' 109 + import {SelectVideoBtn} from '#/view/com/composer/videos/SelectVideoBtn' 110 + import {SubtitleDialogBtn} from '#/view/com/composer/videos/SubtitleDialog' 111 + import {VideoPreview} from '#/view/com/composer/videos/VideoPreview' 112 + import {VideoTranscodeProgress} from '#/view/com/composer/videos/VideoTranscodeProgress' 113 + import {QuoteEmbed, QuoteX} from '#/view/com/util/post-embeds/QuoteEmbed' 114 + import {Text} from '#/view/com/util/text/Text' 115 + import * as Toast from '#/view/com/util/Toast' 116 + import {UserAvatar} from '#/view/com/util/UserAvatar' 90 117 import {atoms as a, native, useTheme} from '#/alf' 91 118 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 92 119 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' ··· 94 121 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 95 122 import * as Prompt from '#/components/Prompt' 96 123 import {Text as NewText} from '#/components/Typography' 97 - import {QuoteEmbed, QuoteX} from '../util/post-embeds/QuoteEmbed' 98 - import {Text} from '../util/text/Text' 99 - import * as Toast from '../util/Toast' 100 - import {UserAvatar} from '../util/UserAvatar' 101 - import {CharProgress} from './char-progress/CharProgress' 102 - import {ExternalEmbed} from './ExternalEmbed' 103 - import {GifAltText} from './GifAltText' 104 - import {LabelsBtn} from './labels/LabelsBtn' 105 - import {Gallery} from './photos/Gallery' 106 - import {OpenCameraBtn} from './photos/OpenCameraBtn' 107 - import {SelectGifBtn} from './photos/SelectGifBtn' 108 - import {SelectPhotoBtn} from './photos/SelectPhotoBtn' 109 - import {SelectLangBtn} from './select-language/SelectLangBtn' 110 - import {SuggestedLanguage} from './select-language/SuggestedLanguage' 111 - // TODO: Prevent naming components that coincide with RN primitives 112 - // due to linting false positives 113 - import {TextInput, TextInputRef} from './text-input/TextInput' 114 - import {ThreadgateBtn} from './threadgate/ThreadgateBtn' 115 - import {useExternalLinkFetch} from './useExternalLinkFetch' 116 - import {SelectVideoBtn} from './videos/SelectVideoBtn' 117 - import {SubtitleDialogBtn} from './videos/SubtitleDialog' 118 - import {VideoPreview} from './videos/VideoPreview' 119 - import {VideoTranscodeProgress} from './videos/VideoTranscodeProgress' 120 124 121 125 type CancelRef = { 122 126 onPressCancel: () => void ··· 578 582 keyboardVerticalOffset={keyboardVerticalOffset} 579 583 style={a.flex_1}> 580 584 <View style={[a.flex_1, viewStyles]} aria-modal accessibilityViewIsModal> 581 - <Animated.View style={topBarAnimatedStyle}> 585 + <Animated.View 586 + style={topBarAnimatedStyle} 587 + layout={native(LinearTransition)}> 582 588 <View style={styles.topbarInner}> 583 589 <Button 584 590 label={_(msg`Cancel`)} ··· 662 668 </Text> 663 669 </View> 664 670 )} 665 - {(error !== '' || videoUploadState.error) && ( 666 - <View style={[a.px_lg, a.pb_sm]}> 667 - <View 668 - style={[ 669 - a.px_md, 670 - a.py_sm, 671 - a.rounded_sm, 672 - a.flex_row, 673 - a.gap_sm, 674 - t.atoms.bg_contrast_25, 675 - { 676 - paddingRight: 48, 677 - }, 678 - ]}> 679 - <CircleInfo fill={t.palette.negative_400} /> 680 - <NewText style={[a.flex_1, a.leading_snug, {paddingTop: 1}]}> 681 - {error || videoUploadState.error} 682 - </NewText> 683 - <Button 684 - label={_(msg`Dismiss error`)} 685 - size="tiny" 686 - color="secondary" 687 - variant="ghost" 688 - shape="round" 689 - style={[ 690 - a.absolute, 691 - { 692 - top: a.py_sm.paddingTop, 693 - right: a.px_md.paddingRight, 694 - }, 695 - ]} 696 - onPress={() => { 697 - if (error) setError('') 698 - else videoUploadDispatch({type: 'Reset'}) 699 - }}> 700 - <ButtonIcon icon={X} /> 701 - </Button> 702 - </View> 703 - </View> 704 - )} 671 + <ErrorBanner 672 + error={error} 673 + videoUploadState={videoUploadState} 674 + clearError={() => setError('')} 675 + videoUploadDispatch={videoUploadDispatch} 676 + /> 705 677 </Animated.View> 706 678 <Animated.ScrollView 679 + layout={native(LinearTransition)} 707 680 onScroll={scrollHandler} 708 681 style={styles.scrollView} 709 682 keyboardShouldPersistTaps="always" ··· 1082 1055 borderTopWidth: StyleSheet.hairlineWidth, 1083 1056 }, 1084 1057 }) 1058 + 1059 + function ErrorBanner({ 1060 + error: standardError, 1061 + videoUploadState, 1062 + clearError, 1063 + videoUploadDispatch, 1064 + }: { 1065 + error: string 1066 + videoUploadState: VideoUploadState 1067 + clearError: () => void 1068 + videoUploadDispatch: VideoUploadDispatch 1069 + }) { 1070 + const t = useTheme() 1071 + const {_} = useLingui() 1072 + 1073 + const videoError = 1074 + videoUploadState.status !== 'idle' ? videoUploadState.error : undefined 1075 + const error = standardError || videoError 1076 + 1077 + const onClearError = () => { 1078 + if (standardError) { 1079 + clearError() 1080 + } else { 1081 + videoUploadDispatch({type: 'Reset'}) 1082 + } 1083 + } 1084 + 1085 + if (!error) return null 1086 + 1087 + return ( 1088 + <Animated.View 1089 + style={[a.px_lg, a.pb_sm]} 1090 + entering={FadeIn} 1091 + exiting={FadeOut}> 1092 + <View 1093 + style={[ 1094 + a.px_md, 1095 + a.py_sm, 1096 + a.gap_xs, 1097 + a.rounded_sm, 1098 + t.atoms.bg_contrast_25, 1099 + ]}> 1100 + <View style={[a.relative, a.flex_row, a.gap_sm, {paddingRight: 48}]}> 1101 + <CircleInfo fill={t.palette.negative_400} /> 1102 + <NewText style={[a.flex_1, a.leading_snug, {paddingTop: 1}]}> 1103 + {error} 1104 + </NewText> 1105 + <Button 1106 + label={_(msg`Dismiss error`)} 1107 + size="tiny" 1108 + color="secondary" 1109 + variant="ghost" 1110 + shape="round" 1111 + style={[a.absolute, {top: 0, right: 0}]} 1112 + onPress={onClearError}> 1113 + <ButtonIcon icon={X} /> 1114 + </Button> 1115 + </View> 1116 + {videoError && videoUploadState.jobStatus?.jobId && ( 1117 + <NewText 1118 + style={[ 1119 + {paddingLeft: 28}, 1120 + a.text_xs, 1121 + a.font_bold, 1122 + a.leading_snug, 1123 + t.atoms.text_contrast_low, 1124 + ]}> 1125 + <Trans>Job ID: {videoUploadState.jobStatus.jobId}</Trans> 1126 + </NewText> 1127 + )} 1128 + </View> 1129 + </Animated.View> 1130 + ) 1131 + } 1085 1132 1086 1133 function ToolbarWrapper({ 1087 1134 style,