Ensure once one type of media is selected, you can only select more of that type

(cherry picked from commit 1a9e6e0cdb5234667f08e3dd9107ae598941fc23)
This commit is contained in:
Eric Bailey
2025-08-13 16:14:07 -05:00
committed by Samuel Newman
parent 9b8fd2eb48
commit d498eeee46
2 changed files with 53 additions and 20 deletions
+18
View File
@@ -133,6 +133,7 @@ import * as toast from '#/components/Toast'
import {Text as NewText} from '#/components/Typography' import {Text as NewText} from '#/components/Typography'
import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
import { import {
type AssetType,
SelectMediaButton, SelectMediaButton,
type SelectMediaButtonProps, type SelectMediaButtonProps,
} from './SelectMediaButton' } from './SelectMediaButton'
@@ -1267,6 +1268,13 @@ function ComposerFooter({
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
/*
* Once we've allowed a certain type of asset to be selected, we don't allow
* other types of media to be selected.
*/
const [selectedAssetsType, setSelectedAssetsType] = useState<
AssetType | undefined
>(undefined)
const media = post.embed.media const media = post.embed.media
const images = media?.type === 'images' ? media.images : [] const images = media?.type === 'images' ? media.images : []
@@ -1304,8 +1312,17 @@ function ComposerFooter({
[dispatch], [dispatch],
) )
/*
* Reset if the user clears any selected media
*/
if (selectedAssetsType !== undefined && !media) {
setSelectedAssetsType(undefined)
}
const onSelectAssets = useCallback<SelectMediaButtonProps['onSelectAssets']>( const onSelectAssets = useCallback<SelectMediaButtonProps['onSelectAssets']>(
async ({type, assets, errors}) => { async ({type, assets, errors}) => {
setSelectedAssetsType(type)
if (assets.length) { if (assets.length) {
if (type === 'image') { if (type === 'image') {
const images: ComposerImage[] = [] const images: ComposerImage[] = []
@@ -1363,6 +1380,7 @@ function ComposerFooter({
<ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}> <ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
<SelectMediaButton <SelectMediaButton
disabled={isMediaSelectionDisabled} disabled={isMediaSelectionDisabled}
allowedAssetTypes={selectedAssetsType}
selectedAssetsCount={selectedAssetsCount} selectedAssetsCount={selectedAssetsCount}
onSelectAssets={onSelectAssets} onSelectAssets={onSelectAssets}
/> />
+35 -20
View File
@@ -26,6 +26,10 @@ import * as toast from '#/components/Toast'
export type SelectMediaButtonProps = { export type SelectMediaButtonProps = {
disabled?: boolean disabled?: boolean
/**
* If set, this limits the types of assets that can be selected.
*/
allowedAssetTypes: AssetType | undefined
selectedAssetsCount: number selectedAssetsCount: number
onSelectAssets: (props: { onSelectAssets: (props: {
type: AssetType type: AssetType
@@ -37,7 +41,7 @@ export type SelectMediaButtonProps = {
/** /**
* Generic asset classes, or buckets, that we support. * Generic asset classes, or buckets, that we support.
*/ */
type AssetType = 'video' | 'image' | 'gif' export type AssetType = 'video' | 'image' | 'gif'
/** /**
* Shadows `ImagePickerAsset` from `expo-image-picker`, but with a guaranteed `mimeType` * Shadows `ImagePickerAsset` from `expo-image-picker`, but with a guaranteed `mimeType`
@@ -231,9 +235,11 @@ async function getAdditionalVideoMetadata(asset: ValidatedImagePickerAsset) {
async function processImagePickerAssets( async function processImagePickerAssets(
assets: ImagePickerAsset[], assets: ImagePickerAsset[],
{ {
selectionLimit, selectionCountRemaining,
allowedAssetTypes,
}: { }: {
selectionLimit: number selectionCountRemaining: number
allowedAssetTypes: AssetType | undefined
}, },
) { ) {
/* /*
@@ -242,10 +248,11 @@ async function processImagePickerAssets(
const errors = new Set<SelectedAssetError>() const errors = new Set<SelectedAssetError>()
/* /*
* We only support selecting a single type of media at a time, so this * We only support selecting a single type of media at a time, so this gets
* gets set to whatever the first asset type is. * set to whatever the first valid asset type is, OR to whatever
* `allowedAssetTypes` is set to.
*/ */
let primaryMediaType: AssetType | undefined let selectableAssetType: AssetType | undefined
/* /*
* This will hold the assets that we can actually use, after filtering * This will hold the assets that we can actually use, after filtering
@@ -260,11 +267,15 @@ async function processImagePickerAssets(
continue continue
} }
// set the primary media type to the first valid asset type /*
primaryMediaType = primaryMediaType || type * If we have an `allowedAssetTypes` prop, constrain to that. Otherwise,
* set this to the first valid asset type we see, and then use that to
* constrain all remaining selected assets.
*/
selectableAssetType = allowedAssetTypes || selectableAssetType || type
// ignore mixed types // ignore mixed types
if (type !== primaryMediaType) { if (type !== selectableAssetType) {
errors.add(SelectedAssetError.MixedTypes) errors.add(SelectedAssetError.MixedTypes)
continue continue
} }
@@ -303,12 +314,12 @@ async function processImagePickerAssets(
}) })
} }
if (primaryMediaType === 'image') { if (selectableAssetType === 'image') {
if (supportedAssets.length > selectionLimit) { if (supportedAssets.length > selectionCountRemaining) {
errors.add(SelectedAssetError.MaxImages) errors.add(SelectedAssetError.MaxImages)
supportedAssets = supportedAssets.slice(0, selectionLimit) supportedAssets = supportedAssets.slice(0, selectionCountRemaining)
} }
} else if (primaryMediaType === 'video') { } else if (selectableAssetType === 'video') {
if (supportedAssets.length > 1) { if (supportedAssets.length > 1) {
errors.add(SelectedAssetError.MaxVideos) errors.add(SelectedAssetError.MaxVideos)
supportedAssets = supportedAssets.slice(0, 1) supportedAssets = supportedAssets.slice(0, 1)
@@ -346,7 +357,7 @@ async function processImagePickerAssets(
errors.add(SelectedAssetError.VideoTooLong) errors.add(SelectedAssetError.VideoTooLong)
supportedAssets = [] supportedAssets = []
} }
} else if (primaryMediaType === 'gif') { } else if (selectableAssetType === 'gif') {
if (supportedAssets.length > 1) { if (supportedAssets.length > 1) {
errors.add(SelectedAssetError.MaxGIFs) errors.add(SelectedAssetError.MaxGIFs)
supportedAssets = supportedAssets.slice(0, 1) supportedAssets = supportedAssets.slice(0, 1)
@@ -354,7 +365,7 @@ async function processImagePickerAssets(
} }
return { return {
type: primaryMediaType!, // set above type: selectableAssetType!, // set above
assets: supportedAssets, assets: supportedAssets,
errors, errors,
} }
@@ -362,6 +373,7 @@ async function processImagePickerAssets(
export function SelectMediaButton({ export function SelectMediaButton({
disabled, disabled,
allowedAssetTypes,
selectedAssetsCount, selectedAssetsCount,
onSelectAssets, onSelectAssets,
}: SelectMediaButtonProps) { }: SelectMediaButtonProps) {
@@ -371,7 +383,7 @@ export function SelectMediaButton({
const sheetWrapper = useSheetWrapper() const sheetWrapper = useSheetWrapper()
const t = useTheme() const t = useTheme()
const selectionLimit = MAX_IMAGES - selectedAssetsCount const selectionCountRemaining = MAX_IMAGES - selectedAssetsCount
const processSelectedAssets = useCallback( const processSelectedAssets = useCallback(
async (rawAssets: ImagePickerAsset[]) => { async (rawAssets: ImagePickerAsset[]) => {
@@ -379,7 +391,10 @@ export function SelectMediaButton({
type, type,
assets, assets,
errors: errorCodes, errors: errorCodes,
} = await processImagePickerAssets(rawAssets, {selectionLimit}) } = await processImagePickerAssets(rawAssets, {
selectionCountRemaining,
allowedAssetTypes,
})
/* /*
* Convert error codes to user-friendly messages. * Convert error codes to user-friendly messages.
@@ -420,7 +435,7 @@ export function SelectMediaButton({
errors, errors,
}) })
}, },
[_, onSelectAssets, selectionLimit], [_, onSelectAssets, selectionCountRemaining, allowedAssetTypes],
) )
const onPressSelectMedia = useCallback(async () => { const onPressSelectMedia = useCallback(async () => {
@@ -445,7 +460,7 @@ export function SelectMediaButton({
quality: 1, quality: 1,
allowsMultipleSelection: true, allowsMultipleSelection: true,
legacy: true, legacy: true,
selectionLimit: isIOS ? selectionLimit : undefined, selectionLimit: isIOS ? selectionCountRemaining : undefined,
preferredAssetRepresentationMode: preferredAssetRepresentationMode:
UIImagePickerPreferredAssetRepresentationMode.Current, UIImagePickerPreferredAssetRepresentationMode.Current,
}), }),
@@ -460,7 +475,7 @@ export function SelectMediaButton({
requestVideoAccessIfNeeded, requestVideoAccessIfNeeded,
sheetWrapper, sheetWrapper,
processSelectedAssets, processSelectedAssets,
selectionLimit, selectionCountRemaining,
]) ])
return ( return (