Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
78 changes: 19 additions & 59 deletions src/components/AttachmentComposerModal.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import React, {memo, useCallback, useEffect, useRef, useState} from 'react';
import type {View} from 'react-native';
import {InteractionManager} from 'react-native';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
import type {OnyxEntry} from 'react-native-onyx';
import Animated, {FadeIn, LayoutAnimationConfig} from 'react-native-reanimated';
import type {ValueOf} from 'type-fest';
import useFilesValidation from '@hooks/useFilesValidation';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import {cleanFileName, getFileValidationErrorText} from '@libs/fileDownload/FileUtils';
import {cleanFileName} from '@libs/fileDownload/FileUtils';
import CONST from '@src/CONST';
import type * as OnyxTypes from '@src/types/onyx';
import type ModalType from '@src/types/utils/ModalType';
Expand All @@ -19,7 +17,6 @@ import useCarouselArrows from './Attachments/AttachmentCarousel/useCarouselArrow
import useAttachmentErrors from './Attachments/AttachmentView/useAttachmentErrors';
import type {Attachment} from './Attachments/types';
import Button from './Button';
import ConfirmModal from './ConfirmModal';
import HeaderGap from './HeaderGap';
import HeaderWithBackButton from './HeaderWithBackButton';
import Modal from './Modal';
Expand All @@ -37,7 +34,7 @@ type ImagePickerResponse = {
type FileObject = Partial<File | ImagePickerResponse>;

type ChildrenProps = {
displayFilesInModal: (data: FileObject[]) => void;
displayFilesInModal: (data: FileObject[], items?: DataTransferItem[]) => void;
show: () => void;
};

Expand Down Expand Up @@ -72,15 +69,12 @@ function AttachmentComposerModal({onConfirm, onModalShow = () => {}, onModalHide
const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();

const [isModalOpen, setIsModalOpen] = useState(false);
const [fileError, setFileError] = useState<ValueOf<typeof CONST.FILE_VALIDATION_ERRORS> | null>(null);
const [isFileErrorModalVisible, setIsFileErrorModalVisible] = useState(false);
const [modalType, setModalType] = useState<ModalType>(CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE);
const [validFilesToUpload, setValidFilesToUpload] = useState<FileObject[]>([]);
const [attachments, setAttachments] = useState<Attachment[]>([]);
const [page, setPage] = useState<number>(0);
const [currentAttachment, setCurrentAttachment] = useState<Attachment | null>(null);

// TODO: remove unnecessary logic, ideally in a follow-up PR to avoid breaking changes/regressions
/**
* If our attachment is a PDF, return the unswipeable Modal type.
*/
Expand Down Expand Up @@ -112,20 +106,6 @@ function AttachmentComposerModal({onConfirm, onModalShow = () => {}, onModalHide
setIsModalOpen(false);
}, [isModalOpen, onConfirm, validFilesToUpload, currentAttachment]);

const closeConfirmModal = useCallback(() => {
setIsFileErrorModalVisible(false);
}, []);

// TODO: Check if this function is still needed, as it doesn't work.
const isDirectoryCheck = useCallback((data: FileObject) => {
if ('webkitGetAsEntry' in data && (data as DataTransferItem).webkitGetAsEntry()?.isDirectory) {
setFileError(CONST.FILE_VALIDATION_ERRORS.FOLDER_NOT_ALLOWED);
setIsFileErrorModalVisible(true);
return false;
}
return true;
}, []);

/**
* Sanitizes file names and ensures proper URI references for file system compatibility
*/
Expand Down Expand Up @@ -159,7 +139,7 @@ function AttachmentComposerModal({onConfirm, onModalShow = () => {}, onModalHide
return;
}

if (validFilesToUpload.length > 0 && !fileError) {
if (validFilesToUpload.length > 0) {
// Convert all files to attachments
const newAttachments = validFilesToUpload.map((fileObject) => {
const source = fileObject.uri ?? '';
Expand All @@ -178,57 +158,47 @@ function AttachmentComposerModal({onConfirm, onModalShow = () => {}, onModalHide

setIsModalOpen(true);
}
}, [fileError, validFilesToUpload, convertFileToAttachment, getModalType]);
}, [validFilesToUpload, convertFileToAttachment, getModalType]);

const {ErrorModal, validateFiles, PDFValidationComponent} = useFilesValidation(setValidFilesToUpload, false);

const confirmAndContinue = () => {
if (fileError === CONST.FILE_VALIDATION_ERRORS.MAX_FILE_LIMIT_EXCEEDED) {
validateFiles(validFilesToUpload);
}
setIsFileErrorModalVisible(false);
InteractionManager.runAfterInteractions(() => {
setFileError(null);
});
};

const validateAndDisplayMultipleFilesToUpload = useCallback(
(data: FileObject[]) => {
(data: FileObject[], items?: DataTransferItem[]) => {
if (!data?.length) {
return;
}

const validIndices: number[] = [];
const fileObjects = data
.map((item) => {
.map((item, index) => {
let fileObject = item;
if ('getAsFile' in item && typeof item.getAsFile === 'function') {
fileObject = item.getAsFile() as FileObject;
}
return cleanFileObjectName(fileObject);
const cleanedFileObject = cleanFileObjectName(fileObject);
if (cleanedFileObject !== null) {
validIndices.push(index);
}
return cleanedFileObject;
})
.filter((fileObject): fileObject is FileObject => fileObject !== null);

if (!fileObjects.length || fileObjects.some((fileObject) => !isDirectoryCheck(fileObject))) {
if (!fileObjects.length) {
return;
}
validateFiles(fileObjects);

// Create a filtered items array that matches the fileObjects
const filteredItems = items && validIndices.length > 0 ? validIndices.map((index) => items.at(index) ?? ({} as DataTransferItem)) : undefined;

validateFiles(fileObjects, filteredItems);
},
[cleanFileObjectName, isDirectoryCheck, validateFiles],
[cleanFileObjectName, validateFiles],
);

const closeModal = useCallback(() => {
setIsModalOpen(false);
}, []);

const closeAndResetModal = useCallback(() => {
closeConfirmModal();
closeModal();
InteractionManager.runAfterInteractions(() => {
setFileError(null);
setValidFilesToUpload([]);
});
}, [closeConfirmModal, closeModal]);

const openModal = useCallback(() => {
setIsModalOpen(true);
}, []);
Expand Down Expand Up @@ -318,16 +288,6 @@ function AttachmentComposerModal({onConfirm, onModalShow = () => {}, onModalHide
</LayoutAnimationConfig>
</GestureHandlerRootView>
</Modal>
<ConfirmModal
title={getFileValidationErrorText(fileError).title}
onConfirm={confirmAndContinue}
onCancel={closeAndResetModal}
isVisible={isFileErrorModalVisible}
prompt={getFileValidationErrorText(fileError).reason}
confirmText={translate(validFilesToUpload.length ? 'common.continue' : 'common.close')}
shouldShowCancelButton={!!validFilesToUpload.length}
cancelText={translate('common.cancel')}
/>
{children?.({
displayFilesInModal: validateAndDisplayMultipleFilesToUpload,
show: openModal,
Expand Down
111 changes: 3 additions & 108 deletions src/components/AttachmentModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {Str} from 'expensify-common';
import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {InteractionManager, Keyboard, View} from 'react-native';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
Expand All @@ -14,7 +13,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
import attachmentModalHandler from '@libs/AttachmentModalHandler';
import fileDownload from '@libs/fileDownload';
import {cleanFileName, getFileName, validateImageForCorruption} from '@libs/fileDownload/FileUtils';
import {getFileName} from '@libs/fileDownload/FileUtils';
import Navigation from '@libs/Navigation/Navigation';
import {getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {hasEReceipt, hasMissingSmartscanFields, hasReceipt, hasReceiptSource, isReceiptBeingScanned} from '@libs/TransactionUtils';
Expand All @@ -28,7 +27,6 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type ModalType from '@src/types/utils/ModalType';
import viewRef from '@src/types/utils/viewRef';
import AttachmentCarousel from './Attachments/AttachmentCarousel';
import AttachmentCarouselPagerContext from './Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext';
Expand Down Expand Up @@ -63,7 +61,6 @@ type ImagePickerResponse = {
type FileObject = Partial<File | ImagePickerResponse>;

type ChildrenProps = {
displayFileInModal: (data: FileObject) => void;
show: () => void;
};

Expand Down Expand Up @@ -202,7 +199,6 @@ function AttachmentModal({
const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState<TranslationPaths | null>(null);
const [attachmentInvalidReason, setAttachmentInvalidReason] = useState<TranslationPaths | null>(null);
const [sourceState, setSourceState] = useState<AvatarSource>(() => source);
const [modalType, setModalType] = useState<ModalType>(CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE);
const [isConfirmButtonDisabled, setIsConfirmButtonDisabled] = useState(false);
const [isDownloadButtonReadyToBeShown, setIsDownloadButtonReadyToBeShown] = React.useState(true);
const isPDFLoadError = useRef(false);
Expand All @@ -219,8 +215,6 @@ function AttachmentModal({
const [currentAttachmentLink, setCurrentAttachmentLink] = useState(attachmentLink);
const {setAttachmentError, isErrorInAttachment, clearAttachmentErrors} = useAttachmentErrors();

// TODO: Remove the logic for validating files, ideally in a follow-up to avoid breaking changes/regressions

const [file, setFile] = useState<FileObject | undefined>(
originalFileName
? {
Expand Down Expand Up @@ -251,17 +245,6 @@ function AttachmentModal({
[onCarouselAttachmentChange],
);

/**
* If our attachment is a PDF, return the unswipeable Modal type.
*/
const getModalType = useCallback(
(sourceURL: string, fileObject: FileObject) =>
sourceURL && (Str.isPDF(sourceURL) || (fileObject && Str.isPDF(fileObject.name ?? translate('attachmentView.unknownFilename'))))
? CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE
: CONST.MODAL.MODAL_TYPE.CENTERED,
[translate],
);

const setDownloadButtonVisibility = useCallback(
(isButtonVisible: boolean) => {
if (isDownloadButtonReadyToBeShown === isButtonVisible) {
Expand Down Expand Up @@ -326,91 +309,6 @@ function AttachmentModal({
Navigation.goBack();
}, [transaction]);

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

if (fileObject.size && fileObject.size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
setIsAttachmentInvalid(true);
setAttachmentInvalidReasonTitle('attachmentPicker.attachmentTooSmall');
setAttachmentInvalidReason('attachmentPicker.sizeNotMet');
return false;
}

return true;
})
.catch(() => {
setIsAttachmentInvalid(true);
setAttachmentInvalidReasonTitle('attachmentPicker.attachmentError');
setAttachmentInvalidReason('attachmentPicker.errorWhileSelectingCorruptedAttachment');
return false;
}),
[],
);

const isDirectoryCheck = useCallback((data: FileObject) => {
if ('webkitGetAsEntry' in data && (data as DataTransferItem).webkitGetAsEntry()?.isDirectory) {
setIsAttachmentInvalid(true);
setAttachmentInvalidReasonTitle('attachmentPicker.attachmentError');
setAttachmentInvalidReason('attachmentPicker.folderNotAllowedMessage');
return false;
}
return true;
}, []);

const validateAndDisplayFileToUpload = useCallback(
(data: FileObject) => {
if (!data || !isDirectoryCheck(data)) {
return;
}
let fileObject = data;
if ('getAsFile' in data && typeof data.getAsFile === 'function') {
fileObject = data.getAsFile() as FileObject;
}
if (!fileObject) {
return;
}

isValidFile(fileObject).then((isValid) => {
if (!isValid) {
return;
}
if (fileObject instanceof File) {
/**
* Cleaning file name, done here so that it covers all cases:
* upload, drag and drop, copy-paste
*/
let updatedFile = fileObject;
const cleanName = cleanFileName(updatedFile.name);
if (updatedFile.name !== cleanName) {
updatedFile = new File([updatedFile], cleanName, {type: updatedFile.type});
}
const inputSource = URL.createObjectURL(updatedFile);
updatedFile.uri = inputSource;
const inputModalType = getModalType(inputSource, updatedFile);
setIsModalOpen(true);
setSourceState(inputSource);
setFile(updatedFile);
setModalType(inputModalType);
} else if (fileObject.uri) {
const inputModalType = getModalType(fileObject.uri, fileObject);
setIsModalOpen(true);
setSourceState(fileObject.uri);
setFile(fileObject);
setModalType(inputModalType);
}
});
},
[isValidFile, getModalType, isDirectoryCheck],
);

/**
* Closes the modal.
* @param {boolean} [shouldCallDirectly] If true, directly calls `onModalClose`.
Expand Down Expand Up @@ -520,7 +418,7 @@ function AttachmentModal({
return (
<>
<Modal
type={modalType}
type={CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@koko57 Why do you change this line?

I see a comment

If our attachment is a PDF, return the unswipeable Modal type.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because modal type was chosen right after validation. As we removed validation it will always be CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE (as a default state value) so we don't need to have a state for that as it won't be changed anymore

onClose={isOverlayModalVisible ? closeConfirmModal : closeModal}
isVisible={isModalOpen}
onModalShow={() => {
Expand Down Expand Up @@ -701,10 +599,7 @@ function AttachmentModal({
/>
)}

{children?.({
displayFileInModal: validateAndDisplayFileToUpload,
show: openModal,
})}
{children?.({show: openModal})}
</>
);
}
Expand Down
Loading