Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create upload picker component #51104

Merged
merged 14 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,9 @@ const CONST = {
},
},
NON_USD_BANK_ACCOUNT: {
ALLOWED_FILE_TYPES: ['pdf', 'jpg', 'jpeg', 'png'],
FILE_LIMIT: 10,
TOTAL_FILES_SIZE_LIMIT: 5242880,
STEP: {
COUNTRY: 'CountryStep',
BANK_INFO: 'BankInfoStep',
Expand Down
162 changes: 89 additions & 73 deletions src/components/AttachmentPicker/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,46 +33,46 @@ type Item = {
pickAttachment: () => Promise<Asset[] | void | DocumentPickerResponse[]>;
};

/**
* See https://github.com/react-native-image-picker/react-native-image-picker/#options
* for ImagePicker configuration options
*/
const imagePickerOptions: Partial<CameraOptions | ImageLibraryOptions> = {
includeBase64: false,
saveToPhotos: false,
selectionLimit: 1,
includeExtra: false,
assetRepresentationMode: 'current',
};

/**
* Return imagePickerOptions based on the type
*/
const getImagePickerOptions = (type: string): CameraOptions => {
const getImagePickerOptions = (type: string, fileLimit: number): CameraOptions | ImageLibraryOptions => {
// mediaType property is one of the ImagePicker configuration to restrict types'
const mediaType = type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE ? 'photo' : 'mixed';

/**
* See https://github.com/react-native-image-picker/react-native-image-picker/#options
* for ImagePicker configuration options
*/
return {
mediaType,
...imagePickerOptions,
includeBase64: false,
saveToPhotos: false,
includeExtra: false,
assetRepresentationMode: 'current',
selectionLimit: fileLimit,
};
};

/**
* Return documentPickerOptions based on the type
* @param {String} type
* @param {Number} fileLimit
* @returns {Object}
*/

const getDocumentPickerOptions = (type: string): DocumentPickerOptions => {
const getDocumentPickerOptions = (type: string, fileLimit: number): DocumentPickerOptions => {
if (type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE) {
return {
type: [RNDocumentPicker.types.images],
copyTo: 'cachesDirectory',
allowMultiSelection: fileLimit !== 1,
};
}
return {
type: [RNDocumentPicker.types.allFiles],
copyTo: 'cachesDirectory',
allowMultiSelection: fileLimit !== 1,
};
};

Expand Down Expand Up @@ -111,13 +111,14 @@ function AttachmentPicker({
type = CONST.ATTACHMENT_PICKER_TYPE.FILE,
children,
shouldHideCameraOption = false,
shouldHideGalleryOption = false,
shouldValidateImage = true,
shouldHideGalleryOption = false,
fileLimit = 1,
}: AttachmentPickerProps) {
const styles = useThemeStyles();
const [isVisible, setIsVisible] = useState(false);

const completeAttachmentSelection = useRef<(data: FileObject) => void>(() => {});
const completeAttachmentSelection = useRef<(data: FileObject[]) => void>(() => {});
const onModalHide = useRef<() => void>();
const onCanceled = useRef<() => void>(() => {});
const popoverRef = useRef(null);
Expand All @@ -143,7 +144,7 @@ function AttachmentPicker({
const showImagePicker = useCallback(
(imagePickerFunc: (options: CameraOptions, callback: Callback) => Promise<ImagePickerResponse>): Promise<Asset[] | void> =>
new Promise((resolve, reject) => {
imagePickerFunc(getImagePickerOptions(type), (response: ImagePickerResponse) => {
imagePickerFunc(getImagePickerOptions(type, fileLimit), (response: ImagePickerResponse) => {
if (response.didCancel) {
// When the user cancelled resolve with no attachment
return resolve();
Expand Down Expand Up @@ -200,7 +201,7 @@ function AttachmentPicker({
}
});
}),
[showGeneralAlert, type],
[fileLimit, showGeneralAlert, type],
);
/**
* Launch the DocumentPicker. Results are in the same format as ImagePicker
Expand All @@ -209,15 +210,15 @@ function AttachmentPicker({
*/
const showDocumentPicker = useCallback(
(): Promise<DocumentPickerResponse[] | void> =>
RNDocumentPicker.pick(getDocumentPickerOptions(type)).catch((error: Error) => {
RNDocumentPicker.pick(getDocumentPickerOptions(type, fileLimit)).catch((error: Error) => {
if (RNDocumentPicker.isCancel(error)) {
return;
}

showGeneralAlert(error.message);
throw error;
}),
[showGeneralAlert, type],
[fileLimit, showGeneralAlert, type],
);

const menuItemData: Item[] = useMemo(() => {
Expand Down Expand Up @@ -261,7 +262,7 @@ function AttachmentPicker({
* @param onPickedHandler A callback that will be called with the selected attachment
* @param onCanceledHandler A callback that will be called without a selected attachment
*/
const open = (onPickedHandler: (file: FileObject) => void, onCanceledHandler: () => void = () => {}) => {
const open = (onPickedHandler: (files: FileObject[]) => void, onCanceledHandler: () => void = () => {}) => {
// eslint-disable-next-line react-compiler/react-compiler
completeAttachmentSelection.current = onPickedHandler;
onCanceled.current = onCanceledHandler;
Expand All @@ -286,7 +287,7 @@ function AttachmentPicker({
}
return getDataForUpload(fileData)
.then((result) => {
completeAttachmentSelection.current(result);
completeAttachmentSelection.current([result]);
})
.catch((error: Error) => {
showGeneralAlert(error.message);
Expand All @@ -301,63 +302,78 @@ function AttachmentPicker({
* sends the selected attachment to the caller (parent component)
*/
const pickAttachment = useCallback(
(attachments: Asset[] | DocumentPickerResponse[] | void = []): Promise<void> | undefined => {
(attachments: Asset[] | DocumentPickerResponse[] | void = []): Promise<void[]> | undefined => {
if (!attachments || attachments.length === 0) {
onCanceled.current();
return Promise.resolve();
return Promise.resolve([]);
}
const fileData = attachments[0];

if (!fileData) {
onCanceled.current();
return Promise.resolve();
}
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
const fileDataName = ('fileName' in fileData && fileData.fileName) || ('name' in fileData && fileData.name) || '';
const fileDataUri = ('fileCopyUri' in fileData && fileData.fileCopyUri) || ('uri' in fileData && fileData.uri) || '';

const fileDataObject: FileResponse = {
name: fileDataName ?? '',
uri: fileDataUri,
size: ('size' in fileData && fileData.size) || ('fileSize' in fileData && fileData.fileSize) || null,
type: fileData.type ?? '',
width: ('width' in fileData && fileData.width) || undefined,
height: ('height' in fileData && fileData.height) || undefined,
};
const filesToProcess = attachments.map((fileData) => {
if (!fileData) {
onCanceled.current();
return Promise.resolve();
}

if (!shouldValidateImage && fileDataName && Str.isImage(fileDataName)) {
ImageSize.getSize(fileDataUri)
.then(({width, height}) => {
fileDataObject.width = width;
fileDataObject.height = height;
return fileDataObject;
})
.then((file) => {
getDataForUpload(file)
.then((result) => {
completeAttachmentSelection.current(result);
})
.catch((error: Error) => {
showGeneralAlert(error.message);
throw error;
});
});
return;
}
/* eslint-enable @typescript-eslint/prefer-nullish-coalescing */
if (fileDataName && Str.isImage(fileDataName)) {
ImageSize.getSize(fileDataUri)
.then(({width, height}) => {
fileDataObject.width = width;
fileDataObject.height = height;
validateAndCompleteAttachmentSelection(fileDataObject);
})
.catch(() => showImageCorruptionAlert());
} else {
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
const fileDataName = ('fileName' in fileData && fileData.fileName) || ('name' in fileData && fileData.name) || '';
const fileDataUri = ('fileCopyUri' in fileData && fileData.fileCopyUri) || ('uri' in fileData && fileData.uri) || '';

const fileDataObject: FileResponse = {
name: fileDataName ?? '',
uri: fileDataUri,
size: ('size' in fileData && fileData.size) || ('fileSize' in fileData && fileData.fileSize) || null,
type: fileData.type ?? '',
width: ('width' in fileData && fileData.width) || undefined,
height: ('height' in fileData && fileData.height) || undefined,
};

if (!shouldValidateImage && fileDataName && Str.isImage(fileDataName)) {
return ImageSize.getSize(fileDataUri)
.then(({width, height}) => {
fileDataObject.width = width;
fileDataObject.height = height;
return fileDataObject;
})
.then((file) => {
return getDataForUpload(file)
.then((result) => completeAttachmentSelection.current([result]))
.catch((error) => {
if (error instanceof Error) {
showGeneralAlert(error.message);
} else {
showGeneralAlert('An unknown error occurred');
}
throw error;
});
})
.catch(() => {
showImageCorruptionAlert();
});
}

if (fileDataName && Str.isImage(fileDataName)) {
return ImageSize.getSize(fileDataUri)
.then(({width, height}) => {
fileDataObject.width = width;
fileDataObject.height = height;

if (fileDataObject.width <= 0 || fileDataObject.height <= 0) {
showImageCorruptionAlert();
return Promise.resolve(); // Skip processing this corrupted file
}

return validateAndCompleteAttachmentSelection(fileDataObject);
})
.catch(() => {
showImageCorruptionAlert();
});
}
return validateAndCompleteAttachmentSelection(fileDataObject);
}
});

return Promise.all(filesToProcess);
},
[validateAndCompleteAttachmentSelection, showImageCorruptionAlert, shouldValidateImage, showGeneralAlert],
[shouldValidateImage, validateAndCompleteAttachmentSelection, showGeneralAlert, showImageCorruptionAlert],
);

/**
Expand Down
8 changes: 5 additions & 3 deletions src/components/AttachmentPicker/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, {useRef} from 'react';
import type {ValueOf} from 'type-fest';
import type {FileObject} from '@components/AttachmentModal';
import * as Browser from '@libs/Browser';
import Visibility from '@libs/Visibility';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -42,9 +43,9 @@ function getAcceptableFileTypesFromAList(fileTypes: Array<ValueOf<typeof CONST.A
* on a Browser we must append a hidden input to the DOM
* and listen to onChange event.
*/
function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE, acceptedFileTypes}: AttachmentPickerProps): React.JSX.Element {
function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE, acceptedFileTypes, allowMultiple = false}: AttachmentPickerProps): React.JSX.Element {
const fileInput = useRef<HTMLInputElement>(null);
const onPicked = useRef<(file: File) => void>(() => {});
const onPicked = useRef<(files: FileObject[]) => void>(() => {});
const onCanceled = useRef<() => void>(() => {});

return (
Expand All @@ -62,7 +63,7 @@ function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE, a

if (file) {
file.uri = URL.createObjectURL(file);
onPicked.current(file);
onPicked.current([file]);
}

// Cleanup after selecting a file to start from a fresh state
Expand Down Expand Up @@ -97,6 +98,7 @@ function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE, a
);
}}
accept={acceptedFileTypes ? getAcceptableFileTypesFromAList(acceptedFileTypes) : getAcceptableFileTypes(type)}
multiple={allowMultiple}
/>
{children({
openPicker: ({onPicked: newOnPicked, onCanceled: newOnCanceled = () => {}}) => {
Expand Down
10 changes: 8 additions & 2 deletions src/components/AttachmentPicker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type {FileObject} from '@components/AttachmentModal';
import type CONST from '@src/CONST';

type PickerOptions = {
/** A callback that will be called with the selected attachment. */
onPicked: (file: FileObject) => void;
/** A callback that will be called with the selected attachments. */
onPicked: (files: FileObject[]) => void;
/** A callback that will be called without a selected attachment. */
onCanceled?: () => void;
};
Expand Down Expand Up @@ -49,6 +49,12 @@ type AttachmentPickerProps = {

/** Whether to validate the image and show the alert or not. */
shouldValidateImage?: boolean;

/** Allow multiple file selection */
allowMultiple?: boolean;

/** Whether to allow multiple files to be selected. */
fileLimit?: number;
};

export default AttachmentPickerProps;
8 changes: 4 additions & 4 deletions src/components/AvatarWithImagePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type ErrorData = {
};

type OpenPickerParams = {
onPicked: (image: FileObject) => void;
onPicked: (image: FileObject[]) => void;
};
type OpenPicker = (args: OpenPickerParams) => void;

Expand Down Expand Up @@ -278,7 +278,7 @@ function AvatarWithImagePicker({
return;
}
openPicker({
onPicked: showAvatarCropModal,
onPicked: (data) => showAvatarCropModal(data.at(0) ?? {}),
});
},
shouldCallAfterModalHide: true,
Expand Down Expand Up @@ -324,7 +324,7 @@ function AvatarWithImagePicker({
}
if (isUsingDefaultAvatar) {
openPicker({
onPicked: showAvatarCropModal,
onPicked: (data) => showAvatarCropModal(data.at(0) ?? {}),
});
return;
}
Expand Down Expand Up @@ -426,7 +426,7 @@ function AvatarWithImagePicker({
// by the user on Safari.
if (index === 0 && Browser.isSafari()) {
openPicker({
onPicked: showAvatarCropModal,
onPicked: (data) => showAvatarCropModal(data.at(0) ?? {}),
});
}
}}
Expand Down
Loading
Loading