diff options
author | Hailey <me@haileyok.com> | 2024-07-30 08:25:31 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-07-30 08:25:31 -0700 |
commit | 8ddb28d3c54b63fb81ca361e741e5a6a46c1d25f (patch) | |
tree | b62aa2dbfb2b9b8d84efbe9ff2a4ef6124dc1bed /src/view/com/composer/Composer.tsx | |
parent | 43ba0f21f6796ebbdd0156c9fa89ebc7d56376e7 (diff) | |
download | voidsky-8ddb28d3c54b63fb81ca361e741e5a6a46c1d25f.tar.zst |
[Video] Uploads (#4754)
* state for video uploads * get upload working * add a debug log * add post progress * progress * fetch data * add some progress info, web uploads * post on finished uploading (wip) * add a note * add some todos * clear video * merge some stuff * convert to `createUploadTask` * patch expo modules core * working native upload progress * platform fork * upload progress for web * cleanup * cleanup * more tweaks * simplify * fix type errors --------- Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
Diffstat (limited to 'src/view/com/composer/Composer.tsx')
-rw-r--r-- | src/view/com/composer/Composer.tsx | 193 |
1 files changed, 136 insertions, 57 deletions
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 72b6fae5f..08ce4441f 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -13,10 +13,16 @@ import { Keyboard, KeyboardAvoidingView, LayoutChangeEvent, + StyleProp, StyleSheet, View, + ViewStyle, } from 'react-native' +// @ts-expect-error no type definition +import ProgressCircle from 'react-native-progress/Circle' import Animated, { + FadeIn, + FadeOut, interpolateColor, useAnimatedStyle, useSharedValue, @@ -55,6 +61,7 @@ import { import {useProfileQuery} from '#/state/queries/profile' import {Gif} from '#/state/queries/tenor' import {ThreadgateSetting} from '#/state/queries/threadgate' +import {useUploadVideo} from '#/state/queries/video/video' import {useAgent, useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' import {useAnalytics} from 'lib/analytics/analytics' @@ -70,6 +77,7 @@ 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 {atoms as a, useTheme} from '#/alf' @@ -96,7 +104,6 @@ import {TextInput, TextInputRef} from './text-input/TextInput' import {ThreadgateBtn} from './threadgate/ThreadgateBtn' import {useExternalLinkFetch} from './useExternalLinkFetch' import {SelectVideoBtn} from './videos/SelectVideoBtn' -import {useVideoState} from './videos/state' import {VideoPreview} from './videos/VideoPreview' import {VideoTranscodeProgress} from './videos/VideoTranscodeProgress' @@ -159,14 +166,21 @@ export const ComposePost = observer(function ComposePost({ const [quote, setQuote] = useState<ComposerOpts['quote'] | undefined>( initQuote, ) + const { - video, - onSelectVideo, - videoPending, - videoProcessingData, + selectVideo, clearVideo, - videoProcessingProgress, - } = useVideoState({setError}) + state: videoUploadState, + } = useUploadVideo({ + setStatus: (status: string) => setProcessingState(status), + onSuccess: () => { + if (publishOnUpload) { + onPressPublish(true) + } + }, + }) + const [publishOnUpload, setPublishOnUpload] = useState(false) + const {extLink, setExtLink} = useExternalLinkFetch({setQuote}) const [extGif, setExtGif] = useState<Gif>() const [labels, setLabels] = useState<string[]>([]) @@ -274,7 +288,7 @@ export const ComposePost = observer(function ComposePost({ return false }, [gallery.needsAltText, extLink, extGif, requireAltTextEnabled]) - const onPressPublish = async () => { + const onPressPublish = async (finishedUploading?: boolean) => { if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) { return } @@ -283,6 +297,15 @@ export const ComposePost = observer(function ComposePost({ return } + if ( + !finishedUploading && + videoUploadState.status !== 'idle' && + videoUploadState.asset + ) { + setPublishOnUpload(true) + return + } + setError('') if ( @@ -387,8 +410,12 @@ export const ComposePost = observer(function ComposePost({ : _(msg`What's up?`) const canSelectImages = - gallery.size < 4 && !extLink && !video && !videoPending - const hasMedia = gallery.size > 0 || Boolean(extLink) || Boolean(video) + gallery.size < 4 && + !extLink && + videoUploadState.status === 'idle' && + !videoUploadState.video + const hasMedia = + gallery.size > 0 || Boolean(extLink) || Boolean(videoUploadState.video) const onEmojiButtonPress = useCallback(() => { openPicker?.(textInput.current?.getCursorPosition()) @@ -500,7 +527,10 @@ export const ComposePost = observer(function ComposePost({ shape="default" size="small" style={[a.rounded_full, a.py_sm]} - onPress={onPressPublish}> + onPress={() => onPressPublish()} + disabled={ + videoUploadState.status !== 'idle' && publishOnUpload + }> <ButtonText style={[a.text_md]}> {replyTo ? ( <Trans context="action">Reply</Trans> @@ -572,7 +602,7 @@ export const ComposePost = observer(function ComposePost({ autoFocus setRichText={setRichText} onPhotoPasted={onPhotoPasted} - onPressPublish={onPressPublish} + onPressPublish={() => onPressPublish()} onNewLink={onNewLink} onError={setError} accessible={true} @@ -602,29 +632,33 @@ export const ComposePost = observer(function ComposePost({ </View> )} - {quote ? ( - <View style={[s.mt5, s.mb2, isWeb && s.mb10]}> - <View style={{pointerEvents: 'none'}}> - <QuoteEmbed quote={quote} /> + <View style={[a.mt_md]}> + {quote ? ( + <View style={[s.mt5, s.mb2, isWeb && s.mb10]}> + <View style={{pointerEvents: 'none'}}> + <QuoteEmbed quote={quote} /> + </View> + {quote.uri !== initQuote?.uri && ( + <QuoteX onRemove={() => setQuote(undefined)} /> + )} </View> - {quote.uri !== initQuote?.uri && ( - <QuoteX onRemove={() => setQuote(undefined)} /> - )} - </View> - ) : null} - {videoPending && videoProcessingData ? ( - <VideoTranscodeProgress - input={videoProcessingData} - progress={videoProcessingProgress} - /> - ) : ( - video && ( + ) : null} + {videoUploadState.status === 'compressing' && + videoUploadState.asset ? ( + <VideoTranscodeProgress + asset={videoUploadState.asset} + progress={videoUploadState.progress} + /> + ) : videoUploadState.video ? ( // remove suspense when we get rid of lazy <Suspense fallback={null}> - <VideoPreview video={video} clear={clearVideo} /> + <VideoPreview + video={videoUploadState.video} + clear={clearVideo} + /> </Suspense> - ) - )} + ) : null} + </View> </Animated.ScrollView> <SuggestedLanguage text={richtext.text} /> @@ -641,33 +675,37 @@ export const ComposePost = observer(function ComposePost({ t.atoms.border_contrast_medium, styles.bottomBar, ]}> - <View style={[a.flex_row, a.align_center, a.gap_xs]}> - <SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} /> - {gate('videos') && ( - <SelectVideoBtn - onSelectVideo={onSelectVideo} - disabled={!canSelectImages} + {videoUploadState.status !== 'idle' ? ( + <VideoUploadToolbar state={videoUploadState} /> + ) : ( + <ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}> + <SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} /> + {gate('videos') && ( + <SelectVideoBtn + onSelectVideo={selectVideo} + disabled={!canSelectImages} + /> + )} + <OpenCameraBtn gallery={gallery} disabled={!canSelectImages} /> + <SelectGifBtn + onClose={focusTextInput} + onSelectGif={onSelectGif} + disabled={hasMedia} /> - )} - <OpenCameraBtn gallery={gallery} disabled={!canSelectImages} /> - <SelectGifBtn - onClose={focusTextInput} - onSelectGif={onSelectGif} - disabled={hasMedia} - /> - {!isMobile ? ( - <Button - onPress={onEmojiButtonPress} - style={a.p_sm} - label={_(msg`Open emoji picker`)} - accessibilityHint={_(msg`Open emoji picker`)} - variant="ghost" - shape="round" - color="primary"> - <EmojiSmile size="lg" /> - </Button> - ) : null} - </View> + {!isMobile ? ( + <Button + onPress={onEmojiButtonPress} + style={a.p_sm} + label={_(msg`Open emoji picker`)} + accessibilityHint={_(msg`Open emoji picker`)} + variant="ghost" + shape="round" + color="primary"> + <EmojiSmile size="lg" /> + </Button> + ) : null} + </ToolbarWrapper> + )} <View style={a.flex_1} /> <SelectLangBtn /> <CharProgress count={graphemeLength} /> @@ -893,3 +931,44 @@ const styles = StyleSheet.create({ borderTopWidth: StyleSheet.hairlineWidth, }, }) + +function ToolbarWrapper({ + style, + children, +}: { + style: StyleProp<ViewStyle> + children: React.ReactNode +}) { + if (isWeb) return children + return ( + <Animated.View + style={style} + entering={FadeIn.duration(400)} + exiting={FadeOut.duration(400)}> + {children} + </Animated.View> + ) +} + +function VideoUploadToolbar({state}: {state: VideoUploadState}) { + const t = useTheme() + + const progress = + state.status === 'compressing' || state.status === 'uploading' + ? state.progress + : state.jobStatus?.progress ?? 100 + + return ( + <ToolbarWrapper + style={[a.gap_sm, a.flex_row, a.align_center, {paddingVertical: 5}]}> + <ProgressCircle + size={30} + borderWidth={1} + borderColor={t.atoms.border_contrast_low.borderColor} + color={t.palette.primary_500} + progress={progress} + /> + <Text>{state.status}</Text> + </ToolbarWrapper> + ) +} |