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

[TS migration] Migrate 'AttachmentPicker' component to TypeScript #37810

Merged
merged 36 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d64a997
Migrate 'AttachmentPicker' component to TypeScript
dukenv0307 Mar 4, 2024
049f950
fix: type in launch camera
dukenv0307 Mar 4, 2024
d34c62b
fix type select item function
dukenv0307 Mar 5, 2024
cd2681e
fix type
dukenv0307 Mar 6, 2024
e8f9f8f
Merge branch 'main' into fix/25134
dukenv0307 Mar 6, 2024
2dda80f
fix: type attachment
dukenv0307 Mar 6, 2024
9ac6e89
fix lint
dukenv0307 Mar 6, 2024
ea868c3
Merge branch 'main' into fix/25134
dukenv0307 Mar 11, 2024
1e3abba
fix type launch camera
dukenv0307 Mar 11, 2024
fc29a73
fix: lint
dukenv0307 Mar 11, 2024
d4401b8
Merge branch 'main' into fix/25134
dukenv0307 Mar 13, 2024
8e5a242
fix type attachment picker
dukenv0307 Mar 13, 2024
564b7d5
fix lint
dukenv0307 Mar 13, 2024
1ab9c3c
fix type attachment
dukenv0307 Mar 13, 2024
7d4f876
Merge branch 'main' into fix/25134
dukenv0307 Mar 15, 2024
1450ea9
fix type attchment picker
dukenv0307 Mar 15, 2024
b729ae6
fix type attachment picker
dukenv0307 Mar 18, 2024
5c2f60a
Merge branch 'main' into fix/25134
dukenv0307 Mar 19, 2024
d73fe5d
fix: fallback in show general alert
dukenv0307 Mar 19, 2024
397feff
Merge branch 'main' into fix/25134
dukenv0307 Mar 20, 2024
8d405ff
fix type attachment picker
dukenv0307 Mar 21, 2024
166a616
fix typecheck
dukenv0307 Mar 21, 2024
d8e484c
Merge branch 'main' into fix/25134
dukenv0307 Mar 21, 2024
1ae4e2b
fix lint
dukenv0307 Mar 21, 2024
78a345e
Update src/components/AttachmentPicker/index.native.tsx
dukenv0307 Mar 25, 2024
9f94ba9
Merge branch 'main' into fix/25134
dukenv0307 Mar 25, 2024
9e39089
fix: eslint
dukenv0307 Mar 25, 2024
d3b891e
Update src/components/AttachmentPicker/types.ts
dukenv0307 Mar 26, 2024
b0fd19e
Add description for each property
dukenv0307 Mar 26, 2024
b41cebe
merge main
dukenv0307 Mar 26, 2024
594fafa
fix type check
dukenv0307 Mar 26, 2024
be0c526
fix type check
dukenv0307 Mar 26, 2024
eba576e
Merge branch 'main' into fix/25134
dukenv0307 Mar 26, 2024
21cb31b
import lanchCamera with alias name
dukenv0307 Mar 26, 2024
9c26f07
add new line
dukenv0307 Mar 26, 2024
640283e
fix lint
dukenv0307 Mar 26, 2024
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
10 changes: 5 additions & 5 deletions src/components/AttachmentModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ type Attachment = {
};

type ImagePickerResponse = {
height: number;
height?: number;
name: string;
size: number;
size?: number | null;
type: string;
uri: string;
width: number;
width?: number;
};

type FileObject = File | ImagePickerResponse;
Expand Down Expand Up @@ -292,14 +292,14 @@ function AttachmentModal({
}, [transaction, report]);

const isValidFile = useCallback((fileObject: FileObject) => {
if (fileObject.size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
if (fileObject.size && fileObject.size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
setIsAttachmentInvalid(true);
setAttachmentInvalidReasonTitle('attachmentPicker.attachmentTooLarge');
setAttachmentInvalidReason('attachmentPicker.sizeExceeded');
return false;
}

if (fileObject.size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
if (fileObject.size && fileObject.size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
setIsAttachmentInvalid(true);
setAttachmentInvalidReasonTitle('attachmentPicker.attachmentTooSmall');
setAttachmentInvalidReason('attachmentPicker.sizeNotMet');
Expand Down
33 changes: 0 additions & 33 deletions src/components/AttachmentPicker/attachmentPickerPropTypes.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import Str from 'expensify-common/lib/str';
import lodashCompact from 'lodash/compact';
import PropTypes from 'prop-types';
import React, {useCallback, useMemo, useRef, useState} from 'react';
import {Alert, Image as RNImage, View} from 'react-native';
import RNFetchBlob from 'react-native-blob-util';
import RNDocumentPicker from 'react-native-document-picker';
import type {DocumentPickerResponse} from 'react-native-document-picker';
import {launchImageLibrary} from 'react-native-image-picker';
import _ from 'underscore';
import type {Asset, Callback, CameraOptions, ImagePickerResponse} from 'react-native-image-picker';
import type {FileObject} from '@components/AttachmentModal';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import Popover from '@components/Popover';
Expand All @@ -17,19 +17,25 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as FileUtils from '@libs/fileDownload/FileUtils';
import CONST from '@src/CONST';
import {defaultProps as baseDefaultProps, propTypes as basePropTypes} from './attachmentPickerPropTypes';
import launchCamera from './launchCamera';

const propTypes = {
...basePropTypes,
import type {TranslationPaths} from '@src/languages/types';
import type IconAsset from '@src/types/utils/IconAsset';
import launchCamera from './launchCamera/launchCamera';
import type BaseAttachmentPickerProps from './types';

type AttachmentPickerProps = BaseAttachmentPickerProps & {
/** If this value is true, then we exclude Camera option. */
shouldHideCameraOption: PropTypes.bool,
shouldHideCameraOption?: boolean;
};

type Item = {
icon: IconAsset;
textTranslationKey: TranslationPaths;
pickAttachment: () => Promise<Asset[] | void | DocumentPickerResponse[]>;
};

const defaultProps = {
...baseDefaultProps,
shouldHideCameraOption: false,
type DocumentPickerOptionsParams = {
type: string[];
copyTo: 'cachesDirectory' | 'documentDirectory';
};

/**
Expand All @@ -45,10 +51,8 @@ const imagePickerOptions = {

/**
* Return imagePickerOptions based on the type
* @param {String} type
* @returns {Object}
*/
const getImagePickerOptions = (type) => {
const getImagePickerOptions = (type: string): CameraOptions => {
// mediaType property is one of the ImagePicker configuration to restrict types'
const mediaType = type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE ? 'photo' : 'mixed';
return {
Expand All @@ -63,7 +67,7 @@ const getImagePickerOptions = (type) => {
* @returns {Object}
*/

const getDocumentPickerOptions = (type) => {
const getDocumentPickerOptions = (type: string): DocumentPickerOptionsParams => {
if (type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE) {
return {
type: [RNDocumentPicker.types.images],
Expand All @@ -79,19 +83,16 @@ const getDocumentPickerOptions = (type) => {
/**
* The data returned from `show` is different on web and mobile, so use this function to ensure the data we
* send to the xhr will be handled properly.
*
* @param {Object} fileData
* @return {Promise}
*/
const getDataForUpload = (fileData) => {
const fileName = fileData.fileName || fileData.name || 'chat_attachment';
const fileResult = {
const getDataForUpload = (fileData: Asset & DocumentPickerResponse): Promise<FileObject> => {
const fileName = fileData.fileName ?? fileData.name ?? 'chat_attachment';
const fileResult: FileObject = {
name: FileUtils.cleanFileName(fileName),
type: fileData.type,
width: fileData.width,
height: fileData.height,
uri: fileData.fileCopyUri || fileData.uri,
size: fileData.fileSize || fileData.size,
uri: fileData.fileCopyUri ?? fileData.uri,
size: fileData.fileSize ?? fileData.size,
};

if (fileResult.size) {
Expand All @@ -109,37 +110,38 @@ const getDataForUpload = (fileData) => {
* returns a "show attachment picker" method that takes
* a callback. This is the ios/android implementation
* opening a modal with attachment options
* @param {propTypes} props
* @returns {JSX.Element}
*/
function AttachmentPicker({type, children, shouldHideCameraOption}) {
function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, shouldHideCameraOption = false}: AttachmentPickerProps) {
const styles = useThemeStyles();
const [isVisible, setIsVisible] = useState(false);

const completeAttachmentSelection = useRef();
const onModalHide = useRef();
const onCanceled = useRef();
const completeAttachmentSelection = useRef<(data: FileObject) => void>(() => {});
const onModalHide = useRef<() => void>();
const onCanceled = useRef<() => void>(() => {});
const popoverRef = useRef(null);

const {translate} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();

/**
* A generic handling when we don't know the exact reason for an error
*/
const showGeneralAlert = useCallback(() => {
Alert.alert(translate('attachmentPicker.attachmentError'), translate('attachmentPicker.errorWhileSelectingAttachment'));
}, [translate]);
const showGeneralAlert = useCallback(
(message = '') => {
Alert.alert(translate('attachmentPicker.attachmentError'), `${message !== '' ? message : translate('attachmentPicker.errorWhileSelectingAttachment')}`);
},
[translate],
);

/**
* Common image picker handling
*
* @param {function} imagePickerFunc - RNImagePicker.launchCamera or RNImagePicker.launchImageLibrary
* @returns {Promise<ImagePickerResponse>}
*/
const showImagePicker = useCallback(
(imagePickerFunc) =>
(imagePickerFunc: (options: CameraOptions, callback: Callback) => Promise<ImagePickerResponse>): Promise<Asset[] | void> =>
new Promise((resolve, reject) => {
imagePickerFunc(getImagePickerOptions(type), (response) => {
imagePickerFunc(getImagePickerOptions(type), (response: ImagePickerResponse) => {
if (response.didCancel) {
// When the user cancelled resolve with no attachment
return resolve();
Expand All @@ -166,10 +168,10 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) {
/**
* Launch the DocumentPicker. Results are in the same format as ImagePicker
*
* @returns {Promise<DocumentPickerResponse[]>}
* @returns {Promise<DocumentPickerResponse[] | void>}
*/
const showDocumentPicker = useCallback(
() =>
(): Promise<DocumentPickerResponse[] | void> =>
RNDocumentPicker.pick(getDocumentPickerOptions(type)).catch((error) => {
if (RNDocumentPicker.isCancel(error)) {
return;
Expand All @@ -181,13 +183,8 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) {
[showGeneralAlert, type],
);

const menuItemData = useMemo(() => {
const data = lodashCompact([
!shouldHideCameraOption && {
icon: Expensicons.Camera,
textTranslationKey: 'attachmentPicker.takePhoto',
pickAttachment: () => showImagePicker(launchCamera),
},
const menuItemData: Item[] = useMemo(() => {
const data: Item[] = [
{
icon: Expensicons.Gallery,
textTranslationKey: 'attachmentPicker.chooseFromGallery',
Expand All @@ -198,7 +195,14 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) {
textTranslationKey: 'attachmentPicker.chooseDocument',
pickAttachment: showDocumentPicker,
},
]);
];
if (!shouldHideCameraOption) {
data.push({
icon: Expensicons.Camera,
textTranslationKey: 'attachmentPicker.takePhoto',
pickAttachment: () => showImagePicker(launchCamera),
});
}

return data;
}, [showDocumentPicker, showImagePicker, shouldHideCameraOption]);
Expand All @@ -215,10 +219,10 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) {
/**
* Opens the attachment modal
*
* @param {function} onPickedHandler A callback that will be called with the selected attachment
* @param {function} onCanceledHandler A callback that will be called without a selected attachment
* @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, onCanceledHandler = () => {}) => {
const open = (onPickedHandler: (file: FileObject) => void, onCanceledHandler: () => void = () => {}) => {
completeAttachmentSelection.current = onPickedHandler;
onCanceled.current = onCanceledHandler;
setIsVisible(true);
Expand All @@ -231,12 +235,8 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) {
setIsVisible(false);
};

/**
* @param {Object} fileData
* @returns {Promise}
*/
const validateAndCompleteAttachmentSelection = useCallback(
(fileData) => {
(fileData: Asset & DocumentPickerResponse) => {
if (fileData.width === -1 || fileData.height === -1) {
showImageCorruptionAlert();
return Promise.resolve();
Expand All @@ -256,22 +256,25 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) {
/**
* Handles the image/document picker result and
* sends the selected attachment to the caller (parent component)
*
* @param {Array<ImagePickerResponse|DocumentPickerResponse>} attachments
* @returns {Promise}
*/
const pickAttachment = useCallback(
(attachments = []) => {
(attachments: Array<(Asset & DocumentPickerResponse) | void> = []): Promise<void> | undefined => {
if (attachments.length === 0) {
onCanceled.current();
return Promise.resolve();
}
const fileData = _.first(attachments);
if (Str.isImage(fileData.fileName || fileData.name)) {
RNImage.getSize(fileData.fileCopyUri || fileData.uri, (width, height) => {

const fileData = attachments[0];

if (!fileData) {
onCanceled.current();
return Promise.resolve();
}
if (fileData.fileName && Str.isImage(fileData.fileName ?? fileData.name)) {
RNImage.getSize(fileData.fileCopyUri ?? fileData.uri, (width, height) => {
fileData.width = width;
fileData.height = height;
return validateAndCompleteAttachmentSelection(fileData);
validateAndCompleteAttachmentSelection(fileData);
});
} else {
return validateAndCompleteAttachmentSelection(fileData);
Expand All @@ -287,20 +290,17 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) {
* @param {Function} item.pickAttachment
*/
const selectItem = useCallback(
(item) => {
(item: Item) => {
/* setTimeout delays execution to the frame after the modal closes
* without this on iOS closing the modal closes the gallery/camera as well */
onModalHide.current = () =>
setTimeout(
() =>
item
.pickAttachment()
.then(pickAttachment)
.catch(console.error)
.finally(() => delete onModalHide.current),
200,
);

onModalHide.current = () => {
setTimeout(() => {
item.pickAttachment()
.then((result) => pickAttachment(result as Array<Asset & DocumentPickerResponse>))
.catch(console.error)
.finally(() => delete onModalHide.current);
}, 200);
};
close();
},
[pickAttachment],
Expand All @@ -322,10 +322,8 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) {

/**
* Call the `children` renderProp with the interface defined in propTypes
*
* @returns {React.ReactNode}
*/
const renderChildren = () =>
const renderChildren = (): React.ReactNode =>
children({
openPicker: ({onPicked, onCanceled: newOnCanceled}) => open(onPicked, newOnCanceled),
});
Expand All @@ -338,11 +336,12 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) {
onCanceled.current();
}}
isVisible={isVisible}
anchorPosition={styles.createMenuPosition}
anchorRef={popoverRef}
// anchorPosition={styles.createMenuPosition}
onModalHide={onModalHide.current}
>
<View style={!isSmallScreenWidth && styles.createMenuContainer}>
{_.map(menuItemData, (item, menuIndex) => (
{menuItemData.map((item, menuIndex) => (
<MenuItem
key={item.textTranslationKey}
icon={item.icon}
Expand All @@ -358,8 +357,6 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) {
);
}

AttachmentPicker.propTypes = propTypes;
AttachmentPicker.defaultProps = defaultProps;
AttachmentPicker.displayName = 'AttachmentPicker';

export default AttachmentPicker;
Loading
Loading