[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>
This commit is contained in:
Hailey 2024-07-30 08:25:31 -07:00 committed by GitHub
parent 43ba0f21f6
commit 8ddb28d3c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 594 additions and 112 deletions

View file

@ -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>
)
}