diff --git a/.gitignore b/.gitignore
index 7233b35472..0bef5d88a1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,6 +58,7 @@ buck-out/
# Ruby / CocoaPods
/ios/Pods/
/vendor/bundle/
+Gemfile.lock
# Testing
coverage/
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 71714fddee..b91009196a 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -40,6 +40,7 @@ import Animated, {
ZoomIn,
ZoomOut,
} from 'react-native-reanimated'
+import {RootSiblingParent} from 'react-native-root-siblings'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {type ImagePickerAsset} from 'expo-image-picker'
import {
@@ -103,7 +104,6 @@ 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
@@ -113,12 +113,10 @@ import {
type TextInputRef,
} from '#/view/com/composer/text-input/TextInput'
import {ThreadgateBtn} from '#/view/com/composer/threadgate/ThreadgateBtn'
-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 {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, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -127,8 +125,10 @@ import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {LazyQuoteEmbed} from '#/components/Post/Embed/LazyQuoteEmbed'
import * as Prompt from '#/components/Prompt'
+import {toast} from '#/components/Toast'
import {Text as NewText} from '#/components/Typography'
import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
+import {SelectMediaBtn} from './SelectMediaBtn'
import {
type ComposerAction,
composerReducer,
@@ -514,13 +514,6 @@ export const ComposePost = ({
onPostSuccess?.(postSuccessData)
}
onClose()
- Toast.show(
- thread.posts.length > 1
- ? _(msg`Your posts have been published`)
- : replyTo
- ? _(msg`Your reply has been published`)
- : _(msg`Your post has been published`),
- )
}, [
_,
agent,
@@ -650,84 +643,88 @@ export const ComposePost = ({
const isWebFooterSticky = !isNative && thread.posts.length > 1
return (
-
-
- 1}
- publishingStage={publishingStage}
- topBarAnimatedStyle={topBarAnimatedStyle}
- onCancel={onPressCancel}
- onPublish={onPressPublish}>
- {missingAltError && }
- setError('')}
- clearVideo={
- erroredVideoPostId
- ? () => clearVideo(erroredVideoPostId)
- : () => {}
- }
- />
-
-
-
- {replyTo ? : undefined}
- {thread.posts.map((post, index) => (
-
- 1}
- isReply={index > 0 || !!replyTo}
- isActive={post.id === activePost.id}
- canRemovePost={thread.posts.length > 1}
- canRemoveQuote={index > 0 || !initQuote}
- onSelectVideo={selectVideo}
- onClearVideo={clearVideo}
- onPublish={onComposerPostPublish}
- onError={setError}
+
+
+
+
+ 1}
+ publishingStage={publishingStage}
+ topBarAnimatedStyle={topBarAnimatedStyle}
+ onCancel={onPressCancel}
+ onPublish={onPressPublish}>
+ {missingAltError && }
+ setError('')}
+ clearVideo={
+ erroredVideoPostId
+ ? () => clearVideo(erroredVideoPostId)
+ : () => {}
+ }
/>
- {isWebFooterSticky && post.id === activePost.id && (
- {footer}
- )}
-
- ))}
-
- {!isWebFooterSticky && footer}
-
+
-
-
+
+ {replyTo ? : undefined}
+ {thread.posts.map((post, index) => (
+
+ 1}
+ isReply={index > 0 || !!replyTo}
+ isActive={post.id === activePost.id}
+ canRemovePost={thread.posts.length > 1}
+ canRemoveQuote={index > 0 || !initQuote}
+ onSelectVideo={selectVideo}
+ onClearVideo={clearVideo}
+ onPublish={onComposerPostPublish}
+ onError={setError}
+ />
+ {isWebFooterSticky && post.id === activePost.id && (
+ {footer}
+ )}
+
+ ))}
+
+ {!isWebFooterSticky && footer}
+
+
+
+
+
+
)
}
@@ -811,7 +808,11 @@ let ComposerPost = React.memo(function ComposerPost({
if (isNative) return // web only
const [mimeType] = uri.slice('data:'.length).split(';')
if (!SUPPORTED_MIME_TYPES.includes(mimeType as SupportedMimeTypes)) {
- Toast.show(_(msg`Unsupported video type`), 'xmark')
+ toast.show({
+ type: 'error',
+ content: _(msg`Unsupported video type`),
+ a11yLabel: _(msg`Unsupported video type.`),
+ })
return
}
const name = `pasted.${mimeToExt(mimeType)}`
@@ -1248,6 +1249,7 @@ function ComposerFooter({
showAddButton,
onEmojiButtonPress,
onError,
+
onSelectVideo,
onAddPost,
}: {
@@ -1267,6 +1269,17 @@ function ComposerFooter({
const images = media?.type === 'images' ? media.images : []
const video = media?.type === 'video' ? media.video : null
const isMaxImages = images.length >= MAX_IMAGES
+ const isMaxVideos = !!video
+
+ let isMediaSelectionDisabled = false
+
+ if (media?.type === 'images') {
+ isMediaSelectionDisabled = isMaxImages
+ } else if (media?.type === 'video') {
+ isMediaSelectionDisabled = isMaxVideos
+ } else {
+ isMediaSelectionDisabled = !!media
+ }
const onImageAdd = useCallback(
(next: ComposerImage[]) => {
@@ -1303,14 +1316,11 @@ function ComposerFooter({
) : (
-
- onSelectVideo(post.id, asset)}
- disabled={!!media}
setError={onError}
/>
void
+ onSelectVideo: (asset: ImagePickerAsset) => void
+ setError: (error: string) => void
+}
+
+// (Note)APiligrim
+// The SelectMediaBtn is responsible for supporting the selection of the following media in the Composer:
+// up to 4 images
+// up to 1 video
+// up to 1 GIF (handled like a video - passed to onSelectVideo)
+
+export function SelectMediaBtn({disabled, onAdd, onSelectVideo}: Props) {
+ const {_} = useLingui()
+ const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
+ const {requestVideoAccessIfNeeded} = useVideoLibraryPermission()
+ const sheetWrapper = useSheetWrapper()
+ const t = useTheme()
+
+ const processSelectedAssets = useCallback(
+ async (assets: ImagePickerAsset[]) => {
+ function performEarlyValidation(assetList: ImagePickerAsset[]) {
+ const images: ImagePickerAsset[] = []
+ const videosAndGifs: ImagePickerAsset[] = []
+ const invalids: ImagePickerAsset[] = []
+
+ // Categorize assets using basic checks (even without complete metadata)
+ for (const asset of assetList) {
+ const ext = asset.uri?.split('.').pop()?.toLowerCase()
+ const isGif = isGifAsset(asset, ext)
+ const isVideo = isVideoAsset(asset)
+ const isImage = isImageAsset(asset)
+
+ if (isGif || isVideo) {
+ // GIFs and videos are handled the same way (fixing regression)
+ videosAndGifs.push(asset)
+ } else if (isImage) {
+ images.push(asset)
+ } else {
+ invalids.push(asset)
+ console.error('Invalid asset detected:', asset)
+ }
+ }
+
+ // Determine media type based on selected assets and preserve selection order
+ let mediaType: 'image' | 'video' | null = null
+ for (const asset of assetList) {
+ const ext = asset.uri?.split('.').pop()?.toLowerCase()
+ const isGif = isGifAsset(asset, ext)
+ const isVideo = isVideoAsset(asset)
+ const isImage = isImageAsset(asset)
+
+ if (isGif || isVideo) {
+ // GIFs are treated like videos
+ mediaType = 'video'
+ break
+ } else if (isImage) {
+ mediaType = 'image'
+ break
+ }
+ }
+
+ let validAssets: ImagePickerAsset[] = []
+ let trimmed = false
+
+ if (mediaType === 'image') {
+ validAssets = images.slice(0, 4)
+ if (images.length > 4) trimmed = true
+
+ if (videosAndGifs.length > 0) {
+ toast.show({
+ type: 'info',
+ content:
+ 'You can select either images or videos. Taking the images.',
+ a11yLabel:
+ 'You can select either images or videos. Taking the images.',
+ })
+ }
+ } else if (mediaType === 'video') {
+ // For videos/GIFs: take only the first one, show toast if mixed media
+ validAssets = videosAndGifs.slice(0, 1)
+
+ if (images.length > 0 || videosAndGifs.length > 1) {
+ toast.show({
+ type: 'info',
+ content:
+ 'You can select either images or videos. Taking the first video.',
+ a11yLabel:
+ 'You can select either images or videos. Taking the first video.',
+ })
+ }
+ }
+
+ return {validAssets, mediaType, trimmed}
+ }
+
+ async function normalizeAssets(assetList: ImagePickerAsset[]) {
+ const normalizedAssets: ImagePickerAsset[] = []
+ const failedAssets: ImagePickerAsset[] = []
+
+ for (const asset of assetList) {
+ try {
+ let normalizedAsset = asset
+
+ // Check if we need to enrich metadata
+ const needsEnrichment =
+ !asset.mimeType ||
+ (asset.type === 'video' && !asset.duration) ||
+ (isGifAsset(asset, asset.uri?.split('.').pop()?.toLowerCase()) &&
+ !asset.duration)
+
+ if (needsEnrichment) {
+ const enrichedAsset = await getMissingMetadata(asset)
+ if (enrichedAsset) {
+ normalizedAsset = enrichedAsset
+ }
+
+ // Last ditch effort: infer mimeType from extension if still missing
+ if (!normalizedAsset.mimeType) {
+ normalizedAsset = {
+ ...normalizedAsset,
+ mimeType:
+ normalizedAsset.type === 'image'
+ ? 'image/jpeg'
+ : inferMimeTypeFromURI(normalizedAsset.uri || ''),
+ }
+ }
+ }
+
+ normalizedAssets.push(normalizedAsset)
+ } catch (error) {
+ console.error('Failed to normalize asset:', asset, error)
+ failedAssets.push(asset)
+ }
+ }
+
+ return {normalizedAssets, failedAssets}
+ }
+
+ function performFinalValidation(
+ assetList: ImagePickerAsset[],
+ mediaType: 'image' | 'video' | null,
+ ) {
+ const validAssets: ImagePickerAsset[] = []
+
+ for (const asset of assetList) {
+ if (mediaType === 'image') {
+ // Images just need basic validation
+ if (asset.mimeType && asset.mimeType.startsWith('image/')) {
+ validAssets.push(asset)
+ } else {
+ console.error('Image failed final validation:', asset)
+ }
+ } else if (mediaType === 'video') {
+ // Videos and GIFs need duration and format checks
+ const isValid = validateVideoOrGIF(asset)
+ if (isValid) {
+ validAssets.push(asset)
+ } else {
+ console.error('Video/GIF failed final validation:', asset)
+ }
+ }
+ }
+
+ return validAssets
+ }
+
+ // 1. Early validation to detect asset format and count limits
+ const {validAssets, mediaType, trimmed} = performEarlyValidation(assets)
+
+ if (validAssets.length === 0) {
+ toast.show({
+ type: 'error',
+ content: 'No valid media files selected',
+ a11yLabel: 'No valid media files selected',
+ })
+ return
+ }
+
+ // Show toast if selected files were trimmed
+ if (trimmed) {
+ toast.show({
+ type: 'info',
+ content: 'Selection limited to first 4 files.',
+ a11yLabel: 'Selection limited to first 4 files.',
+ })
+ }
+
+ // 2. Normalize assets by adding missing metadata
+ const {normalizedAssets, failedAssets} =
+ await normalizeAssets(validAssets)
+
+ if (failedAssets.length > 0) {
+ console.error('Some assets failed to normalize:', failedAssets)
+ if (normalizedAssets.length === 0) {
+ toast.show({
+ type: 'error',
+ content: 'Failed to process selected files',
+ a11yLabel: 'Failed to process selected files',
+ })
+ return
+ } else {
+ toast.show({
+ type: 'info',
+ content: 'Some files could not be processed. Using valid files.',
+ a11yLabel: 'Some files could not be processed. Using valid files.',
+ })
+ }
+ }
+
+ // 3. Final validation checks on normalized assets
+ const finalAssets = performFinalValidation(normalizedAssets, mediaType)
+
+ if (finalAssets.length === 0) {
+ toast.show({
+ type: 'error',
+ content: 'This media type is not supported',
+ a11yLabel: 'This media type is not supported',
+ })
+ return
+ }
+
+ // 4. Add finalized assets to the composer
+ if (mediaType === 'image') {
+ const composerImages = await generateComposerImages(finalAssets)
+ onAdd(composerImages)
+ } else {
+ // Both videos and GIFs get selected with onSelectVideo
+ onSelectVideo(finalAssets[0])
+ }
+ },
+ [onAdd, onSelectVideo],
+ )
+
+ const onPressSelectMedia = useCallback(async () => {
+ if (isNative) {
+ const [photoAccess, videoAccess] = await Promise.all([
+ requestPhotoAccessIfNeeded(),
+ requestVideoAccessIfNeeded(),
+ ])
+
+ if (!photoAccess && !videoAccess) {
+ toast.show({
+ type: 'error',
+ content: 'You need to allow access to your media library.',
+ a11yLabel: 'You need to allow access to your media library.',
+ })
+ return
+ }
+ }
+
+ //APiligrim
+ //Note: selectionLimit doesn't work reliably on Android, so we handle limiting in code
+ const response = await sheetWrapper(
+ launchImageLibraryAsync({
+ exif: false,
+ mediaTypes: ['images', 'videos'],
+ quality: 1,
+ allowsMultipleSelection: true,
+ legacy: true,
+ }),
+ )
+
+ if (!response.assets || response.assets.length === 0) {
+ return
+ }
+
+ await processSelectedAssets(response.assets)
+ }, [
+ requestPhotoAccessIfNeeded,
+ requestVideoAccessIfNeeded,
+ sheetWrapper,
+ processSelectedAssets,
+ ])
+
+ return (
+
+ )
+}
+
+async function generateComposerImages(
+ images: ImagePickerAsset[],
+): Promise {
+ const imageMetas = images.map(image => ({
+ mime: image.mimeType || 'image/jpeg',
+ height: image.height,
+ width: image.width,
+ path: image.uri,
+ size: getDataUriSize(image.uri),
+ }))
+
+ const results = await Promise.all(
+ imageMetas.map(img => createComposerImage(img)),
+ )
+
+ return results
+}
+
+function isGifAsset(asset: ImagePickerAsset, ext?: string): boolean {
+ return asset.mimeType === 'image/gif' || ext === 'gif'
+}
+
+function isVideoAsset(asset: ImagePickerAsset): boolean {
+ if (asset.type === 'video') {
+ return true
+ }
+ if (asset.mimeType && asset.mimeType.startsWith('video/')) {
+ return true
+ }
+ if (asset.uri && /\.(mp4|mov|avi|webm|m4v)$/i.test(asset.uri)) {
+ return true
+ }
+
+ // Check for data URI with video mime type
+ if (asset.uri && asset.uri.startsWith('data:video/')) {
+ return true
+ }
+
+ if (typeof asset.duration === 'number' && asset.duration > 0) {
+ return true
+ }
+
+ return false
+}
+
+function isImageAsset(asset: ImagePickerAsset): boolean {
+ if (asset.type === 'image') return true
+ if (asset.mimeType && asset.mimeType.startsWith('image/')) return true
+ return false
+}
+
+function validateVideoOrGIF(asset: ImagePickerAsset): boolean {
+ if (!asset) {
+ console.error('Asset is null or undefined')
+ return false
+ }
+
+ if (isWeb) {
+ // asset.duration is null for gifs (see the TODO in pickVideo.web.ts)
+ if (asset.duration && asset.duration > VIDEO_MAX_DURATION_MS) {
+ toast.show({
+ type: 'error',
+ content: 'Videos must be less than 3 minutes long',
+ a11yLabel: 'Videos must be less than 3 minutes long',
+ })
+ return false
+ }
+ // compression step on native converts to mp4, so no need to check there
+ if (
+ asset.mimeType &&
+ !SUPPORTED_MIME_TYPES.includes(asset.mimeType as SupportedMimeTypes)
+ ) {
+ toast.show({
+ type: 'error',
+ content: 'This video format is not supported',
+ a11yLabel: 'This video format is not supported',
+ })
+ return false
+ }
+ } else {
+ // Check if asset exists and has duration property before accessing it
+ if (!asset.duration || typeof asset.duration !== 'number') {
+ toast.show({
+ type: 'error',
+ content: 'Please select a valid video file',
+ a11yLabel: 'Please select a valid video file',
+ })
+ return false
+ }
+ if (asset.duration > VIDEO_MAX_DURATION_MS) {
+ toast.show({
+ type: 'error',
+ content: 'Videos must be less than 3 minutes long',
+ a11yLabel: 'Videos must be less than 3 minutes long',
+ })
+ return false
+ }
+ }
+ return true
+}
+
+function inferMimeTypeFromURI(uri: string): string {
+ const ext = uri.split('.').pop()?.toLowerCase()
+ switch (ext) {
+ case 'mp4':
+ return 'video/mp4'
+ case 'mov':
+ return 'video/quicktime'
+ case 'webm':
+ return 'video/webm'
+ case 'avi':
+ return 'video/x-msvideo'
+ case 'gif':
+ return 'image/gif'
+ default:
+ return 'video/mp4'
+ }
+}
+
+async function getMissingMetadata(
+ asset: ImagePickerAsset,
+): Promise {
+ if (!isWeb || !asset.uri) return asset
+
+ if (asset.uri.startsWith('data:')) {
+ try {
+ const mimeTypeMatch = asset.uri.match(/^data:([^;]+)/)
+ const extractedMimeType = mimeTypeMatch ? mimeTypeMatch[1] : null
+
+ const response = await fetch(asset.uri)
+ const blob = await response.blob()
+ const file = new File([blob], 'file', {
+ type: blob.type || extractedMimeType || '',
+ })
+
+ let enrichedAsset = asset
+
+ // If it's a video, try to get video metadata
+ if (
+ extractedMimeType?.startsWith('video/') ||
+ blob.type.startsWith('video/')
+ ) {
+ try {
+ const videoAsset = await getVideoMetadata(file)
+ enrichedAsset = {
+ ...asset,
+ ...videoAsset,
+ mimeType: videoAsset.mimeType || extractedMimeType || blob.type,
+ type: 'video',
+ }
+ } catch (error) {
+ console.error(
+ ' if getting metadata for a video fails, using basic info',
+ error,
+ )
+
+ enrichedAsset = {
+ ...asset,
+ mimeType: extractedMimeType || blob.type,
+ type: extractedMimeType?.startsWith('video/')
+ ? 'video'
+ : asset.type,
+ }
+ }
+ } else {
+ // For non-video assets (images and GIFs), setting the mime type
+ enrichedAsset = {
+ ...asset,
+ mimeType: extractedMimeType || blob.type,
+ }
+ }
+
+ return enrichedAsset
+ } catch (error) {
+ console.error('Failed to extract metadata:', error)
+ return null
+ }
+ }
+
+ return asset
+}
diff --git a/src/view/com/composer/photos/SelectPhotoBtn.tsx b/src/view/com/composer/photos/SelectPhotoBtn.tsx
deleted file mode 100644
index f4c6aa328a..0000000000
--- a/src/view/com/composer/photos/SelectPhotoBtn.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-/* eslint-disable react-native-a11y/has-valid-accessibility-ignores-invert-colors */
-import {useCallback} from 'react'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {usePhotoLibraryPermission} from '#/lib/hooks/usePermissions'
-import {openPicker} from '#/lib/media/picker'
-import {isNative} from '#/platform/detection'
-import {ComposerImage, createComposerImage} from '#/state/gallery'
-import {atoms as a, useTheme} from '#/alf'
-import {Button} from '#/components/Button'
-import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
-import {Image_Stroke2_Corner0_Rounded as Image} from '#/components/icons/Image'
-
-type Props = {
- size: number
- disabled?: boolean
- onAdd: (next: ComposerImage[]) => void
-}
-
-export function SelectPhotoBtn({size, disabled, onAdd}: Props) {
- const {_} = useLingui()
- const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
- const t = useTheme()
- const sheetWrapper = useSheetWrapper()
-
- const onPressSelectPhotos = useCallback(async () => {
- if (isNative && !(await requestPhotoAccessIfNeeded())) {
- return
- }
-
- const images = await sheetWrapper(
- openPicker({
- selectionLimit: 4 - size,
- allowsMultipleSelection: true,
- }),
- )
-
- const results = await Promise.all(
- images.map(img => createComposerImage(img)),
- )
-
- onAdd(results)
- }, [requestPhotoAccessIfNeeded, size, onAdd, sheetWrapper])
-
- return (
-
- )
-}
diff --git a/src/view/com/composer/videos/SelectVideoBtn.tsx b/src/view/com/composer/videos/SelectVideoBtn.tsx
deleted file mode 100644
index 96715955f0..0000000000
--- a/src/view/com/composer/videos/SelectVideoBtn.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import {useCallback} from 'react'
-import {type ImagePickerAsset} from 'expo-image-picker'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {
- SUPPORTED_MIME_TYPES,
- type SupportedMimeTypes,
- VIDEO_MAX_DURATION_MS,
-} from '#/lib/constants'
-import {useVideoLibraryPermission} from '#/lib/hooks/usePermissions'
-import {isWeb} from '#/platform/detection'
-import {isNative} from '#/platform/detection'
-import {atoms as a, useTheme} from '#/alf'
-import {Button} from '#/components/Button'
-import {VideoClip_Stroke2_Corner0_Rounded as VideoClipIcon} from '#/components/icons/VideoClip'
-import {pickVideo} from './pickVideo'
-
-type Props = {
- onSelectVideo: (video: ImagePickerAsset) => void
- disabled?: boolean
- setError: (error: string) => void
-}
-
-export function SelectVideoBtn({onSelectVideo, disabled, setError}: Props) {
- const {_} = useLingui()
- const t = useTheme()
- const {requestVideoAccessIfNeeded} = useVideoLibraryPermission()
-
- const onPressSelectVideo = useCallback(async () => {
- if (isNative && !(await requestVideoAccessIfNeeded())) {
- return
- }
-
- const response = await pickVideo()
- if (response.assets && response.assets.length > 0) {
- const asset = response.assets[0]
- try {
- if (isWeb) {
- // asset.duration is null for gifs (see the TODO in pickVideo.web.ts)
- if (asset.duration && asset.duration > VIDEO_MAX_DURATION_MS) {
- throw Error(_(msg`Videos must be less than 3 minutes long`))
- }
- // compression step on native converts to mp4, so no need to check there
- if (
- !SUPPORTED_MIME_TYPES.includes(asset.mimeType as SupportedMimeTypes)
- ) {
- throw Error(_(msg`Unsupported video type: ${asset.mimeType}`))
- }
- } else {
- if (typeof asset.duration !== 'number') {
- throw Error('Asset is not a video')
- }
- if (asset.duration > VIDEO_MAX_DURATION_MS) {
- throw Error(_(msg`Videos must be less than 3 minutes long`))
- }
- }
- onSelectVideo(asset)
- } catch (err) {
- if (err instanceof Error) {
- setError(err.message)
- } else {
- setError(_(msg`An error occurred while selecting the video`))
- }
- }
- }
- }, [requestVideoAccessIfNeeded, setError, _, onSelectVideo])
-
- return (
- <>
-
- >
- )
-}