Comments and cleanup

(cherry picked from commit e69bd186e7335372f440c446ae6643ed0fb15db9)
This commit is contained in:
Eric Bailey
2025-08-12 16:51:50 -05:00
committed by Samuel Newman
parent c6264508f7
commit a599a379ba
2 changed files with 162 additions and 41 deletions
+1 -1
View File
@@ -1340,7 +1340,7 @@ function ComposerFooter({
errors.map((error, i) => {
toast.show({
type: 'error',
type: 'warning',
content: error,
a11yLabel: error,
duration: 3e3 * (errors.length - i),
+161 -40
View File
@@ -7,14 +7,17 @@ import {
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {VIDEO_MAX_DURATION_MS} from '#/lib/constants'
import {
usePhotoLibraryPermission,
useVideoLibraryPermission,
} from '#/lib/hooks/usePermissions'
import {extractDataUriMime} from '#/lib/media/util'
import {isIOS, isNative} from '#/platform/detection'
import {mimeToExt} from '#/lib/media/video/util'
import {logger} from '#/logger'
import {isIOS, isNative, isWeb} from '#/platform/detection'
import {MAX_IMAGES} from '#/view/com/composer/state/composer'
// import {getVideoMetadata} from '#/view/com/composer/videos/pickVideo'
import {getVideoMetadata} from '#/view/com/composer/videos/pickVideo'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
@@ -38,11 +41,16 @@ export type SelectedAsset = {
type: 'video' | 'image' | 'gif'
}
export type ValidatedImagePickerAsset = Omit<ImagePickerAsset, 'mimeType'> & {
mimeType: string
}
export enum SelectedAssetError {
Unsupported = 'Unsupported',
MixedTypes = 'MixedTypes',
MaxImages = 'MaxImages',
MaxVideos = 'MaxVideos',
VideoTooLong = 'VideoTooLong',
MaxGIFs = 'MaxGIFs',
}
@@ -60,6 +68,7 @@ const SUPPORTED_IMAGE_MIME_TYPES = (
'image/jpeg',
'image/png',
'image/svg+xml',
'image/webp',
isIOS && 'image/heic',
] as const
).filter(Boolean)
@@ -94,9 +103,15 @@ function getImagePickerAssetType(asset: ImagePickerAsset):
type: undefined
mimeType: undefined
} {
/*
* Try to use the `mimeType` reported by Expo's library first.
*/
let mimeType = asset.mimeType
if (!mimeType) {
/*
* We can try to infer this from the data-uri.
*/
const maybeMimeType = extractDataUriMime(asset.uri)
if (
@@ -105,20 +120,41 @@ function getImagePickerAssetType(asset: ImagePickerAsset):
) {
mimeType = maybeMimeType
} else if (maybeMimeType.startsWith('file/')) {
/*
* On the off-chance we get a `file/*` mime, try to infer from the
* extension.
*/
const extension = asset.uri.split('.').pop()?.toLowerCase()
mimeType = extensionToMimeType[extension || '']
}
}
let type: SelectedAsset['type'] | undefined
if (!mimeType) {
return {
success: false,
type: undefined,
mimeType: undefined,
}
}
/*
* We can now do some validation of the `mimeType` and distill it down into
* our supported `type` groups.
*/
let type: SelectedAsset['type'] | undefined
if (mimeType === 'image/gif') {
type = 'gif'
} else if (
mimeType?.startsWith('video/') &&
SUPPORTED_VIDEO_MIME_TYPES.includes(mimeType as SupportedVideoMimeType)
) {
type = 'video'
} else if (mimeType?.startsWith('video/')) {
/**
* We don't care about mimeType at this point on native, since the
* `processVideo` step later on will convert to `.mp4`.
*/
if (
!isWeb ||
SUPPORTED_VIDEO_MIME_TYPES.includes(mimeType as SupportedVideoMimeType)
) {
type = 'video'
}
} else if (
mimeType?.startsWith('image/') &&
SUPPORTED_IMAGE_MIME_TYPES.includes(mimeType as SupportedImageMimeType)
@@ -126,7 +162,16 @@ function getImagePickerAssetType(asset: ImagePickerAsset):
type = 'image'
}
if (!type || !mimeType) {
// console.log({
// asset,
// type,
// mimeType,
// })
/*
* If we weren't able to find a valid type, we don't support this asset.
*/
if (!type) {
return {
success: false,
type: undefined,
@@ -141,6 +186,22 @@ function getImagePickerAssetType(asset: ImagePickerAsset):
}
}
/*
* On web, certain file formats (like `.mov`) don't give us a duration or
* dimensions, so we need to load the file manually to extract this.
*/
async function getAdditionalVideoMetadata(asset: ValidatedImagePickerAsset) {
const file = await fetch(asset.uri)
.then(res => res.blob())
.then(
blob =>
new File([blob], `tmp.${mimeToExt(asset.mimeType)}`, {
type: asset.mimeType,
}),
)
return await getVideoMetadata(file)
}
export function SelectMediaBtn({
disabled,
selectedAssetsCount,
@@ -156,24 +217,36 @@ export function SelectMediaBtn({
const processSelectedAssets = useCallback(
async (assets: ImagePickerAsset[]) => {
const errors = new Set<SelectedAssetError>()
let supportedAssets: ImagePickerAsset[] = []
/*
* A deduped set of error codes, which we'll use later
*/
const errorCodes = new Set<SelectedAssetError>()
/*
* We only support selecting a single type of media at a time, so this
* gets set to whatever the first asset type is.
*/
let primaryMediaType: SelectedAsset['type'] | undefined
/*
* This will hold the assets that we can actually use, after filtering
*/
let supportedAssets: ValidatedImagePickerAsset[] = []
for (const asset of assets) {
const {success, type, mimeType} = getImagePickerAssetType(asset)
if (!success) {
errors.add(SelectedAssetError.Unsupported)
errorCodes.add(SelectedAssetError.Unsupported)
continue
}
// set the primary media type to the first valid asset type
primaryMediaType = primaryMediaType || type
// ignore mixed types
if (type !== primaryMediaType) {
// Selecting a mix of media types is not allowed
errors.add(SelectedAssetError.MixedTypes)
errorCodes.add(SelectedAssetError.MixedTypes)
continue
}
@@ -185,43 +258,91 @@ export function SelectMediaBtn({
if (primaryMediaType === 'image') {
if (supportedAssets.length > selectionLimit) {
errors.add(SelectedAssetError.MaxImages)
errorCodes.add(SelectedAssetError.MaxImages)
supportedAssets = supportedAssets.slice(0, selectionLimit)
}
} else {
} else if (primaryMediaType === 'video') {
if (supportedAssets.length > 1) {
if (primaryMediaType === 'video') {
errors.add(SelectedAssetError.MaxVideos)
} else if (primaryMediaType === 'gif') {
errors.add(SelectedAssetError.MaxGIFs)
}
errorCodes.add(SelectedAssetError.MaxVideos)
supportedAssets = supportedAssets.slice(0, 1)
}
const selectedVideo = supportedAssets[0]
if (typeof selectedVideo.duration !== 'number') {
try {
const metadata = await getAdditionalVideoMetadata(selectedVideo)
selectedVideo.duration = metadata.duration
selectedVideo.width = metadata.width
selectedVideo.height = metadata.height
} catch (e: any) {
logger.error(
`processSelectedAssets: failed to get video metadata`,
{
safeMessage: e.message,
},
)
errorCodes.add(SelectedAssetError.Unsupported)
supportedAssets = []
}
} else {
/*
* The `duration` is in seconds on web, but in milliseconds on
* native. We normalize to milliseconds.
*/
if (isWeb) {
selectedVideo.duration = selectedVideo.duration * 1000
}
}
if (
selectedVideo.duration &&
selectedVideo.duration > VIDEO_MAX_DURATION_MS
) {
errorCodes.add(SelectedAssetError.VideoTooLong)
supportedAssets = []
}
} else if (primaryMediaType === 'gif') {
if (supportedAssets.length > 1) {
errorCodes.add(SelectedAssetError.MaxGIFs)
supportedAssets = supportedAssets.slice(0, 1)
}
}
/*
* Convert error codes to user-friendly messages.
*/
const errors = Array.from(errorCodes).map(error => {
return {
[SelectedAssetError.Unsupported]: _(
msg`One or more of your selected files are not supported.`,
),
[SelectedAssetError.MixedTypes]: _(
msg`Selecting multiple media types is not supported.`,
),
[SelectedAssetError.MaxImages]: _(
msg`You can select up to ${MAX_IMAGES} total images.`,
),
[SelectedAssetError.MaxVideos]: _(
msg`You can only select one video at a time.`,
),
[SelectedAssetError.VideoTooLong]: _(
msg`Videos must be less than 3 minutes long.`,
),
[SelectedAssetError.MaxGIFs]: _(
msg`You can only select one GIF at a time.`,
),
}[error]
})
/*
* Finally, report the selected assets and any errors back to the
* composer.
*/
onSelectAssets({
type: primaryMediaType!,
assets: supportedAssets,
errors: Array.from(errors).map(error => {
return {
[SelectedAssetError.Unsupported]: _(
msg`One or more of your selected files are not supported.`,
),
[SelectedAssetError.MixedTypes]: _(
msg`Selecting multiple media types is not supported.`,
),
[SelectedAssetError.MaxImages]: _(
msg`You can select up to ${MAX_IMAGES} total images.`,
),
[SelectedAssetError.MaxVideos]: _(
msg`You can only select one video at a time.`,
),
[SelectedAssetError.MaxGIFs]: _(
msg`You can only select one GIF at a time.`,
),
}[error]
}),
errors,
})
},
[_, onSelectAssets, selectionLimit],