Comments and cleanup
(cherry picked from commit e69bd186e7335372f440c446ae6643ed0fb15db9)
This commit is contained in:
committed by
Samuel Newman
parent
c6264508f7
commit
a599a379ba
@@ -1340,7 +1340,7 @@ function ComposerFooter({
|
|||||||
|
|
||||||
errors.map((error, i) => {
|
errors.map((error, i) => {
|
||||||
toast.show({
|
toast.show({
|
||||||
type: 'error',
|
type: 'warning',
|
||||||
content: error,
|
content: error,
|
||||||
a11yLabel: error,
|
a11yLabel: error,
|
||||||
duration: 3e3 * (errors.length - i),
|
duration: 3e3 * (errors.length - i),
|
||||||
|
|||||||
@@ -7,14 +7,17 @@ import {
|
|||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {VIDEO_MAX_DURATION_MS} from '#/lib/constants'
|
||||||
import {
|
import {
|
||||||
usePhotoLibraryPermission,
|
usePhotoLibraryPermission,
|
||||||
useVideoLibraryPermission,
|
useVideoLibraryPermission,
|
||||||
} from '#/lib/hooks/usePermissions'
|
} from '#/lib/hooks/usePermissions'
|
||||||
import {extractDataUriMime} from '#/lib/media/util'
|
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 {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 {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
|
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
|
||||||
@@ -38,11 +41,16 @@ export type SelectedAsset = {
|
|||||||
type: 'video' | 'image' | 'gif'
|
type: 'video' | 'image' | 'gif'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ValidatedImagePickerAsset = Omit<ImagePickerAsset, 'mimeType'> & {
|
||||||
|
mimeType: string
|
||||||
|
}
|
||||||
|
|
||||||
export enum SelectedAssetError {
|
export enum SelectedAssetError {
|
||||||
Unsupported = 'Unsupported',
|
Unsupported = 'Unsupported',
|
||||||
MixedTypes = 'MixedTypes',
|
MixedTypes = 'MixedTypes',
|
||||||
MaxImages = 'MaxImages',
|
MaxImages = 'MaxImages',
|
||||||
MaxVideos = 'MaxVideos',
|
MaxVideos = 'MaxVideos',
|
||||||
|
VideoTooLong = 'VideoTooLong',
|
||||||
MaxGIFs = 'MaxGIFs',
|
MaxGIFs = 'MaxGIFs',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +68,7 @@ const SUPPORTED_IMAGE_MIME_TYPES = (
|
|||||||
'image/jpeg',
|
'image/jpeg',
|
||||||
'image/png',
|
'image/png',
|
||||||
'image/svg+xml',
|
'image/svg+xml',
|
||||||
|
'image/webp',
|
||||||
isIOS && 'image/heic',
|
isIOS && 'image/heic',
|
||||||
] as const
|
] as const
|
||||||
).filter(Boolean)
|
).filter(Boolean)
|
||||||
@@ -94,9 +103,15 @@ function getImagePickerAssetType(asset: ImagePickerAsset):
|
|||||||
type: undefined
|
type: undefined
|
||||||
mimeType: undefined
|
mimeType: undefined
|
||||||
} {
|
} {
|
||||||
|
/*
|
||||||
|
* Try to use the `mimeType` reported by Expo's library first.
|
||||||
|
*/
|
||||||
let mimeType = asset.mimeType
|
let mimeType = asset.mimeType
|
||||||
|
|
||||||
if (!mimeType) {
|
if (!mimeType) {
|
||||||
|
/*
|
||||||
|
* We can try to infer this from the data-uri.
|
||||||
|
*/
|
||||||
const maybeMimeType = extractDataUriMime(asset.uri)
|
const maybeMimeType = extractDataUriMime(asset.uri)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -105,20 +120,41 @@ function getImagePickerAssetType(asset: ImagePickerAsset):
|
|||||||
) {
|
) {
|
||||||
mimeType = maybeMimeType
|
mimeType = maybeMimeType
|
||||||
} else if (maybeMimeType.startsWith('file/')) {
|
} 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()
|
const extension = asset.uri.split('.').pop()?.toLowerCase()
|
||||||
mimeType = extensionToMimeType[extension || '']
|
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') {
|
if (mimeType === 'image/gif') {
|
||||||
type = 'gif'
|
type = 'gif'
|
||||||
} else if (
|
} else if (mimeType?.startsWith('video/')) {
|
||||||
mimeType?.startsWith('video/') &&
|
/**
|
||||||
SUPPORTED_VIDEO_MIME_TYPES.includes(mimeType as SupportedVideoMimeType)
|
* We don't care about mimeType at this point on native, since the
|
||||||
) {
|
* `processVideo` step later on will convert to `.mp4`.
|
||||||
type = 'video'
|
*/
|
||||||
|
if (
|
||||||
|
!isWeb ||
|
||||||
|
SUPPORTED_VIDEO_MIME_TYPES.includes(mimeType as SupportedVideoMimeType)
|
||||||
|
) {
|
||||||
|
type = 'video'
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
mimeType?.startsWith('image/') &&
|
mimeType?.startsWith('image/') &&
|
||||||
SUPPORTED_IMAGE_MIME_TYPES.includes(mimeType as SupportedImageMimeType)
|
SUPPORTED_IMAGE_MIME_TYPES.includes(mimeType as SupportedImageMimeType)
|
||||||
@@ -126,7 +162,16 @@ function getImagePickerAssetType(asset: ImagePickerAsset):
|
|||||||
type = 'image'
|
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 {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
type: undefined,
|
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({
|
export function SelectMediaBtn({
|
||||||
disabled,
|
disabled,
|
||||||
selectedAssetsCount,
|
selectedAssetsCount,
|
||||||
@@ -156,24 +217,36 @@ export function SelectMediaBtn({
|
|||||||
|
|
||||||
const processSelectedAssets = useCallback(
|
const processSelectedAssets = useCallback(
|
||||||
async (assets: ImagePickerAsset[]) => {
|
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
|
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) {
|
for (const asset of assets) {
|
||||||
const {success, type, mimeType} = getImagePickerAssetType(asset)
|
const {success, type, mimeType} = getImagePickerAssetType(asset)
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
errors.add(SelectedAssetError.Unsupported)
|
errorCodes.add(SelectedAssetError.Unsupported)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the primary media type to the first valid asset type
|
// set the primary media type to the first valid asset type
|
||||||
primaryMediaType = primaryMediaType || type
|
primaryMediaType = primaryMediaType || type
|
||||||
|
|
||||||
|
// ignore mixed types
|
||||||
if (type !== primaryMediaType) {
|
if (type !== primaryMediaType) {
|
||||||
// Selecting a mix of media types is not allowed
|
errorCodes.add(SelectedAssetError.MixedTypes)
|
||||||
errors.add(SelectedAssetError.MixedTypes)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,43 +258,91 @@ export function SelectMediaBtn({
|
|||||||
|
|
||||||
if (primaryMediaType === 'image') {
|
if (primaryMediaType === 'image') {
|
||||||
if (supportedAssets.length > selectionLimit) {
|
if (supportedAssets.length > selectionLimit) {
|
||||||
errors.add(SelectedAssetError.MaxImages)
|
errorCodes.add(SelectedAssetError.MaxImages)
|
||||||
supportedAssets = supportedAssets.slice(0, selectionLimit)
|
supportedAssets = supportedAssets.slice(0, selectionLimit)
|
||||||
}
|
}
|
||||||
} else {
|
} else if (primaryMediaType === 'video') {
|
||||||
if (supportedAssets.length > 1) {
|
if (supportedAssets.length > 1) {
|
||||||
if (primaryMediaType === 'video') {
|
errorCodes.add(SelectedAssetError.MaxVideos)
|
||||||
errors.add(SelectedAssetError.MaxVideos)
|
supportedAssets = supportedAssets.slice(0, 1)
|
||||||
} else if (primaryMediaType === 'gif') {
|
}
|
||||||
errors.add(SelectedAssetError.MaxGIFs)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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)
|
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({
|
onSelectAssets({
|
||||||
type: primaryMediaType!,
|
type: primaryMediaType!,
|
||||||
assets: supportedAssets,
|
assets: supportedAssets,
|
||||||
errors: Array.from(errors).map(error => {
|
errors,
|
||||||
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]
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[_, onSelectAssets, selectionLimit],
|
[_, onSelectAssets, selectionLimit],
|
||||||
|
|||||||
Reference in New Issue
Block a user