diff options
Diffstat (limited to 'src/view/com')
-rw-r--r-- | src/view/com/composer/Composer.tsx | 124 | ||||
-rw-r--r-- | src/view/com/composer/videos/SelectVideoBtn.tsx | 47 |
2 files changed, 114 insertions, 57 deletions
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index f354f0f0d..185a57fc3 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -36,6 +36,7 @@ import Animated, { ZoomOut, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {ImagePickerAsset} from 'expo-image-picker' import { AppBskyFeedDefs, AppBskyFeedGetPostThread, @@ -82,9 +83,10 @@ import {Gif} from '#/state/queries/tenor' import {ThreadgateAllowUISetting} from '#/state/queries/threadgate' import {threadgateViewToAllowUISetting} from '#/state/queries/threadgate/util' import { + createVideoState, + processVideo, State as VideoUploadState, - useUploadVideo, - VideoUploadDispatch, + videoReducer, } from '#/state/queries/video/video' import {useAgent, useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' @@ -147,7 +149,8 @@ export const ComposePost = ({ }) => { const {currentAccount} = useSession() const agent = useAgent() - const {data: currentProfile} = useProfileQuery({did: currentAccount!.did}) + const currentDid = currentAccount!.did + const {data: currentProfile} = useProfileQuery({did: currentDid}) const {isModalActive} = useModals() const {closeComposer} = useComposerControls() const pal = usePalette('default') @@ -189,21 +192,50 @@ export const ComposePost = ({ const [videoAltText, setVideoAltText] = useState('') const [captions, setCaptions] = useState<{lang: string; file: File}[]>([]) - const { - selectVideo, - clearVideo, - state: videoUploadState, - updateVideoDimensions, - dispatch: videoUploadDispatch, - } = useUploadVideo({ - setStatus: setProcessingState, - onSuccess: () => { - if (publishOnUpload) { - onPressPublish(true) - } + const [videoUploadState, videoDispatch] = useReducer( + videoReducer, + undefined, + createVideoState, + ) + + const selectVideo = React.useCallback( + (asset: ImagePickerAsset) => { + processVideo( + asset, + videoDispatch, + agent, + currentDid, + videoUploadState.abortController.signal, + _, + ) }, - initialVideoUri: initVideoUri, - }) + [_, videoUploadState.abortController, videoDispatch, agent, currentDid], + ) + + // Whenever we receive an initial video uri, we should immediately run compression if necessary + useEffect(() => { + if (initVideoUri) { + selectVideo({uri: initVideoUri} as ImagePickerAsset) + } + }, [initVideoUri, selectVideo]) + + const clearVideo = React.useCallback(() => { + videoUploadState.abortController.abort() + videoDispatch({type: 'to_idle', nextController: new AbortController()}) + }, [videoUploadState.abortController, videoDispatch]) + + const updateVideoDimensions = useCallback( + (width: number, height: number) => { + videoDispatch({ + type: 'update_dimensions', + width, + height, + signal: videoUploadState.abortController.signal, + }) + }, + [videoUploadState.abortController], + ) + const hasVideo = Boolean(videoUploadState.asset || videoUploadState.video) const [publishOnUpload, setPublishOnUpload] = useState(false) @@ -400,19 +432,18 @@ export const ComposePost = ({ postgate, onStateChange: setProcessingState, langs: toPostLanguages(langPrefs.postLanguage), - video: videoUploadState.pendingPublish?.blobRef - ? { - blobRef: videoUploadState.pendingPublish.blobRef, - altText: videoAltText, - captions: captions, - aspectRatio: videoUploadState.asset - ? { - width: videoUploadState.asset?.width, - height: videoUploadState.asset?.height, - } - : undefined, - } - : undefined, + video: + videoUploadState.status === 'done' + ? { + blobRef: videoUploadState.pendingPublish.blobRef, + altText: videoAltText, + captions: captions, + aspectRatio: { + width: videoUploadState.asset.width, + height: videoUploadState.asset.height, + }, + } + : undefined, }) ).uri try { @@ -694,7 +725,7 @@ export const ComposePost = ({ error={error} videoUploadState={videoUploadState} clearError={() => setError('')} - videoUploadDispatch={videoUploadDispatch} + clearVideo={clearVideo} /> </Animated.View> <Animated.ScrollView @@ -1083,25 +1114,25 @@ function ErrorBanner({ error: standardError, videoUploadState, clearError, - videoUploadDispatch, + clearVideo, }: { error: string videoUploadState: VideoUploadState clearError: () => void - videoUploadDispatch: VideoUploadDispatch + clearVideo: () => void }) { const t = useTheme() const {_} = useLingui() const videoError = - videoUploadState.status !== 'idle' ? videoUploadState.error : undefined + videoUploadState.status === 'error' ? videoUploadState.error : undefined const error = standardError || videoError const onClearError = () => { if (standardError) { clearError() } else { - videoUploadDispatch({type: 'Reset'}) + clearVideo() } } @@ -1136,7 +1167,7 @@ function ErrorBanner({ <ButtonIcon icon={X} /> </Button> </View> - {videoError && videoUploadState.jobStatus?.jobId && ( + {videoError && videoUploadState.jobId && ( <NewText style={[ {paddingLeft: 28}, @@ -1145,7 +1176,7 @@ function ErrorBanner({ a.leading_snug, t.atoms.text_contrast_low, ]}> - <Trans>Job ID: {videoUploadState.jobStatus.jobId}</Trans> + <Trans>Job ID: {videoUploadState.jobId}</Trans> </NewText> )} </View> @@ -1174,9 +1205,7 @@ function ToolbarWrapper({ function VideoUploadToolbar({state}: {state: VideoUploadState}) { const t = useTheme() const {_} = useLingui() - const progress = state.jobStatus?.progress - ? state.jobStatus.progress / 100 - : state.progress + const progress = state.progress const shouldRotate = state.status === 'processing' && (progress === 0 || progress === 1) let wheelProgress = shouldRotate ? 0.33 : progress @@ -1212,16 +1241,15 @@ function VideoUploadToolbar({state}: {state: VideoUploadState}) { case 'processing': text = _('Processing video...') break + case 'error': + text = _('Error') + wheelProgress = 100 + break case 'done': text = _('Video uploaded') break } - if (state.error) { - text = _('Error') - wheelProgress = 100 - } - return ( <ToolbarWrapper style={[a.flex_row, a.align_center, {paddingVertical: 5}]}> <Animated.View style={[animatedStyle]}> @@ -1229,7 +1257,11 @@ function VideoUploadToolbar({state}: {state: VideoUploadState}) { size={30} borderWidth={1} borderColor={t.atoms.border_contrast_low.borderColor} - color={state.error ? t.palette.negative_500 : t.palette.primary_500} + color={ + state.status === 'error' + ? t.palette.negative_500 + : t.palette.primary_500 + } progress={wheelProgress} /> </Animated.View> diff --git a/src/view/com/composer/videos/SelectVideoBtn.tsx b/src/view/com/composer/videos/SelectVideoBtn.tsx index 2f2b4c3e7..bbb3d95f2 100644 --- a/src/view/com/composer/videos/SelectVideoBtn.tsx +++ b/src/view/com/composer/videos/SelectVideoBtn.tsx @@ -9,12 +9,14 @@ import { import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {SUPPORTED_MIME_TYPES, SupportedMimeTypes} from '#/lib/constants' +import {BSKY_SERVICE} from '#/lib/constants' import {useVideoLibraryPermission} from '#/lib/hooks/usePermissions' +import {getHostnameFromUrl} from '#/lib/strings/url-helpers' +import {isWeb} from '#/platform/detection' import {isNative} from '#/platform/detection' import {useModalControls} from '#/state/modals' import {useSession} from '#/state/session' -import {BSKY_SERVICE} from 'lib/constants' -import {getHostnameFromUrl} from 'lib/strings/url-helpers' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' import {VideoClip_Stroke2_Corner0_Rounded as VideoClipIcon} from '#/components/icons/VideoClip' @@ -58,16 +60,25 @@ export function SelectVideoBtn({onSelectVideo, disabled, setError}: Props) { UIImagePickerPreferredAssetRepresentationMode.Current, }) if (response.assets && response.assets.length > 0) { - if (isNative) { - if (typeof response.assets[0].duration !== 'number') - throw Error('Asset is not a video') - if (response.assets[0].duration > VIDEO_MAX_DURATION) { - setError(_(msg`Videos must be less than 60 seconds long`)) - return - } - } + const asset = response.assets[0] try { - onSelectVideo(response.assets[0]) + if (isWeb) { + // compression step on native converts to mp4, so no need to check there + const mimeType = getMimeType(asset) + if ( + !SUPPORTED_MIME_TYPES.includes(mimeType as SupportedMimeTypes) + ) { + throw Error(_(msg`Unsupported video type: ${mimeType}`)) + } + } else { + if (typeof asset.duration !== 'number') { + throw Error('Asset is not a video') + } + if (asset.duration > VIDEO_MAX_DURATION) { + throw Error(_(msg`Videos must be less than 60 seconds long`)) + } + } + onSelectVideo(asset) } catch (err) { if (err instanceof Error) { setError(err.message) @@ -132,3 +143,17 @@ function VerifyEmailPrompt({control}: {control: Prompt.PromptControlProps}) { /> ) } + +function getMimeType(asset: ImagePickerAsset) { + if (isWeb) { + const [mimeType] = asset.uri.slice('data:'.length).split(';base64,') + if (!mimeType) { + throw new Error('Could not determine mime type') + } + return mimeType + } + if (!asset.mimeType) { + throw new Error('Could not determine mime type') + } + return asset.mimeType +} |