about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2024-09-06 05:25:56 +0100
committerGitHub <noreply@github.com>2024-09-05 21:25:56 -0700
commit55468595d0dc5f55b9ef58400324832d811ec5d6 (patch)
tree1b66fbbff97fe810e54ffa861f0b1fa6cf62046d /src
parent18133483fe679808671f6d31011594e57fe9a002 (diff)
downloadvoidsky-55468595d0dc5f55b9ef58400324832d811ec5d6.tar.zst
[Video] Error banner improvements (#5163)
Diffstat (limited to 'src')
-rw-r--r--src/state/queries/video/video.ts13
-rw-r--r--src/view/com/composer/Composer.tsx209
2 files changed, 137 insertions, 85 deletions
diff --git a/src/state/queries/video/video.ts b/src/state/queries/video/video.ts
index 4546595e7..06331c886 100644
--- a/src/state/queries/video/video.ts
+++ b/src/state/queries/video/video.ts
@@ -5,6 +5,7 @@ import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
 
+import {AbortError} from '#/lib/async/cancelable'
 import {SUPPORTED_MIME_TYPES, SupportedMimeTypes} from '#/lib/constants'
 import {logger} from '#/logger'
 import {isWeb} from '#/platform/detection'
@@ -39,6 +40,8 @@ export interface State {
   pendingPublish?: {blobRef: BlobRef; mutableProcessed: boolean}
 }
 
+export type VideoUploadDispatch = (action: Action) => void
+
 function reducer(queryClient: QueryClient) {
   return (state: State, action: Action): State => {
     let updatedState = state
@@ -144,8 +147,9 @@ export function useUploadVideo({
       setJobId(response.jobId)
     },
     onError: e => {
-      logger.error('Error uploading video', {safeMessage: e})
-      if (e instanceof ServerError) {
+      if (e instanceof AbortError) {
+        return
+      } else if (e instanceof ServerError) {
         dispatch({
           type: 'SetError',
           error: e.message,
@@ -176,8 +180,9 @@ export function useUploadVideo({
       onVideoCompressed(video)
     },
     onError: e => {
-      logger.error('Error uploading video', {safeMessage: e})
-      if (e instanceof VideoTooLargeError) {
+      if (e instanceof AbortError) {
+        return
+      } else if (e instanceof VideoTooLargeError) {
         dispatch({
           type: 'SetError',
           error: _(msg`The selected video is larger than 100MB.`),
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 3ca709a1e..640602618 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -25,6 +25,7 @@ import Animated, {
   FadeOut,
   interpolateColor,
   LayoutAnimationConfig,
+  LinearTransition,
   useAnimatedStyle,
   useDerivedValue,
   useSharedValue,
@@ -45,18 +46,31 @@ import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 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 {MAX_GRAPHEME_LENGTH} from '#/lib/constants'
 import {
   createGIFDescription,
   parseAltFromGIFDescription,
 } from '#/lib/gif-alt-text'
 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 {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 {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
+import {useDialogStateControlContext} from '#/state/dialogs'
 import {emitPostCreated} from '#/state/events'
 import {useModalControls} from '#/state/modals'
 import {useModals} from '#/state/modals'
+import {GalleryModel} from '#/state/models/media/gallery'
 import {useRequireAltTextEnabled} from '#/state/preferences'
 import {
   toPostLanguages,
@@ -68,25 +82,38 @@ import {useProfileQuery} from '#/state/queries/profile'
 import {Gif} from '#/state/queries/tenor'
 import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
 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 {useComposerControls} from '#/state/shell/composer'
-import {useAnalytics} from 'lib/analytics/analytics'
-import * as apilib from 'lib/api/index'
-import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
-import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-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 {isAndroid, isIOS, isNative, isWeb} from 'platform/detection'
-import {useDialogStateControlContext} from 'state/dialogs'
-import {GalleryModel} from 'state/models/media/gallery'
-import {State as VideoUploadState} from 'state/queries/video/video'
-import {ComposerOpts} from 'state/shell/composer'
-import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo'
+import {ComposerOpts} from '#/state/shell/composer'
+import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
+import {ComposerReplyTo} from '#/view/com/composer/ComposerReplyTo'
+import {ExternalEmbed} from '#/view/com/composer/ExternalEmbed'
+import {GifAltText} from '#/view/com/composer/GifAltText'
+import {LabelsBtn} from '#/view/com/composer/labels/LabelsBtn'
+import {Gallery} from '#/view/com/composer/photos/Gallery'
+import {OpenCameraBtn} from '#/view/com/composer/photos/OpenCameraBtn'
+import {SelectGifBtn} from '#/view/com/composer/photos/SelectGifBtn'
+import {SelectPhotoBtn} from '#/view/com/composer/photos/SelectPhotoBtn'
+import {SelectLangBtn} from '#/view/com/composer/select-language/SelectLangBtn'
+import {SuggestedLanguage} from '#/view/com/composer/select-language/SuggestedLanguage'
+// TODO: Prevent naming components that coincide with RN primitives
+// due to linting false positives
+import {TextInput, TextInputRef} from '#/view/com/composer/text-input/TextInput'
+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 {Button, ButtonIcon, ButtonText} from '#/components/Button'
 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 * as Prompt from '#/components/Prompt'
 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 = {
   onPressCancel: () => void
@@ -578,7 +582,9 @@ export const ComposePost = observer(function ComposePost({
       keyboardVerticalOffset={keyboardVerticalOffset}
       style={a.flex_1}>
       <View style={[a.flex_1, viewStyles]} aria-modal accessibilityViewIsModal>
-        <Animated.View style={topBarAnimatedStyle}>
+        <Animated.View
+          style={topBarAnimatedStyle}
+          layout={native(LinearTransition)}>
           <View style={styles.topbarInner}>
             <Button
               label={_(msg`Cancel`)}
@@ -662,48 +668,15 @@ export const ComposePost = observer(function ComposePost({
               </Text>
             </View>
           )}
-          {(error !== '' || videoUploadState.error) && (
-            <View style={[a.px_lg, a.pb_sm]}>
-              <View
-                style={[
-                  a.px_md,
-                  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>
-          )}
+          <ErrorBanner
+            error={error}
+            videoUploadState={videoUploadState}
+            clearError={() => setError('')}
+            videoUploadDispatch={videoUploadDispatch}
+          />
         </Animated.View>
         <Animated.ScrollView
+          layout={native(LinearTransition)}
           onScroll={scrollHandler}
           style={styles.scrollView}
           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({
   style,
   children,