Skip to content
10 changes: 5 additions & 5 deletions src/hooks/useFilesValidation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const sortFilesByOriginalOrder = (files: FileObject[], orderMap: Map<string, num
return files.sort((a, b) => (orderMap.get(a.uri ?? '') ?? 0) - (orderMap.get(b.uri ?? '') ?? 0));
};

const isImageFile = (file: FileObject) => hasHeicOrHeifExtension(file) ?? Str.isImage(file.name ?? '');
const isImageFile = (file: FileObject): boolean => hasHeicOrHeifExtension(file) || Str.isImage(file.name ?? '');

function useFilesValidation(onFilesValidated: (files: FileObject[], dataTransferItems: DataTransferItem[]) => void) {
const styles = useThemeStyles();
Expand Down Expand Up @@ -166,13 +166,13 @@ function useFilesValidation(onFilesValidated: (files: FileObject[], dataTransfer
return;
}

if (result.error === CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE && isImageFile(file) && validationState.isValidatingReceipts) {
filesToResize.push(file);
if (result.error === CONST.FILE_VALIDATION_ERRORS.HEIC_OR_HEIF_IMAGE) {
filesToConvert.push(file);
return;
}

if (result.error === CONST.FILE_VALIDATION_ERRORS.HEIC_OR_HEIF_IMAGE) {
filesToConvert.push(file);
if (result.error === CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE && isImageFile(file) && validationState.isValidatingReceipts) {
filesToResize.push(file);
return;
}

Expand Down
3 changes: 1 addition & 2 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2883,8 +2883,7 @@ type AttachmentModalScreensParamList = {
accountID?: number;
attachmentID?: string;
source?: AvatarSource;
file?: FileObject | FileObject[];
dataTransferItems?: DataTransferItem[];
file: FileObject | FileObject[];
type?: ValueOf<typeof CONST.ATTACHMENT_TYPE>;
isAuthTokenRequired?: boolean;
originalFileName?: string;
Expand Down
15 changes: 7 additions & 8 deletions src/libs/fileDownload/FileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,9 +632,8 @@ const isValidReceiptExtension = (file: FileObject) => {
);
};

const hasHeicOrHeifExtension = (file: FileObject) => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
return file.name?.toLowerCase().endsWith('.heic') || file.name?.toLowerCase().endsWith('.heif');
const hasHeicOrHeifExtension = (file: FileObject): boolean => {
return (file.name?.toLowerCase().endsWith('.heic') ?? false) || (file.name?.toLowerCase().endsWith('.heif') ?? false);
};

/**
Expand Down Expand Up @@ -706,11 +705,6 @@ const getFileValidationErrorText = (
title: translate('attachmentPicker.someFilesCantBeUploaded'),
reason: translate('attachmentPicker.sizeLimitExceeded', maxSize / 1024 / 1024),
};
case CONST.FILE_VALIDATION_ERRORS.FOLDER_NOT_ALLOWED:
return {
title: translate('attachmentPicker.attachmentError'),
reason: translate('attachmentPicker.folderNotAllowedMessage'),
};
case CONST.FILE_VALIDATION_ERRORS.MAX_FILE_LIMIT_EXCEEDED:
return {
title: translate('attachmentPicker.someFilesCantBeUploaded'),
Expand All @@ -722,6 +716,11 @@ const getFileValidationErrorText = (
}

switch (validationError.error) {
case CONST.FILE_VALIDATION_ERRORS.FOLDER_NOT_ALLOWED:
return {
title: translate('attachmentPicker.attachmentError'),
reason: translate('attachmentPicker.folderNotAllowedMessage'),
};
case CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE:
return {
title: translate('attachmentPicker.wrongFileType'),
Expand Down
35 changes: 13 additions & 22 deletions src/libs/validateAttachmentFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {Str} from 'expensify-common';
import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import type {FileObject} from '@src/types/utils/Attachment';
import {cleanFileName, hasHeicOrHeifExtension, isValidReceiptExtension, normalizeFileObject, validateImageForCorruption} from './fileDownload/FileUtils';
import {cleanFileName, cleanFileObject, hasHeicOrHeifExtension, isValidReceiptExtension, normalizeFileObject, validateImageForCorruption} from './fileDownload/FileUtils';

type ValidateAttachmentValidResult = {
isValid: true;
Expand All @@ -17,40 +17,31 @@ type ValidateAttachmentInvalidResult = {
type ValidateAttachmentResult = ValidateAttachmentValidResult | ValidateAttachmentInvalidResult;

async function validateAttachmentFile(file: FileObject, item?: DataTransferItem, isValidatingReceipts = false): Promise<ValidateAttachmentResult> {
if (!file.name || file.size == null) {
return {isValid: false, error: CONST.FILE_VALIDATION_ERRORS.FILE_INVALID};
}
const fileObject = cleanFileObject(file);

if (isValidatingReceipts && !isValidReceiptExtension(file)) {
if (isValidatingReceipts && !isValidReceiptExtension(fileObject)) {
return {isValid: false, error: CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE};
}

if (hasHeicOrHeifExtension(file)) {
return {isValid: false, error: CONST.FILE_VALIDATION_ERRORS.HEIC_OR_HEIF_IMAGE};
}

const isImage = Str.isImage(file.name);
const maxFileSize = isValidatingReceipts ? CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE : CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE;
if (!isImage && !hasHeicOrHeifExtension(file) && file.size > maxFileSize) {
return {isValid: false, error: CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE};
}

if (isValidatingReceipts && file.size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
return {isValid: false, error: CONST.FILE_VALIDATION_ERRORS.FILE_TOO_SMALL};
if (isDataTransferItemDirectory(item)) {
return {isValid: false, error: CONST.FILE_VALIDATION_ERRORS.FOLDER_NOT_ALLOWED};
}

let fileObject = file;
const fileConverted = file.getAsFile?.();
if (fileConverted) {
fileObject = fileConverted;
}
const fileName = fileObject.name ?? '';
const fileSize = fileObject.size ?? 0;
const isImage = Str.isImage(fileName);

if (!fileObject) {
return {isValid: false, error: CONST.FILE_VALIDATION_ERRORS.FILE_INVALID};
const maxFileSize = isValidatingReceipts ? CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE : CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE;
if (!isImage && fileSize > maxFileSize) {
return {isValid: false, error: CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE};
}

if (isDataTransferItemDirectory(item)) {
return {isValid: false, error: CONST.FILE_VALIDATION_ERRORS.FOLDER_NOT_ALLOWED};
if (isValidatingReceipts && fileSize < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
return {isValid: false, error: CONST.FILE_VALIDATION_ERRORS.FILE_TOO_SMALL};
}

const normalizedFile = await normalizeFileObject(fileObject);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,12 @@ function useAttachmentUploadValidation({

const reportAttachmentsContext = useContext(AttachmentModalContext);
const showAttachmentModalScreen = useCallback(
(file: FileObject | FileObject[], dataTransferItems?: DataTransferItem[]) => {
(file: FileObject[]) => {
const sourceOfFirstAttachment = file.at(0)?.uri;
reportAttachmentsContext.setCurrentAttachment<typeof SCREENS.REPORT_ADD_ATTACHMENT>({
reportID,
source: sourceOfFirstAttachment,
file,
dataTransferItems,
headerTitle: translate('reportActionCompose.sendAttachment'),
onConfirm: addAttachment,
onShow: () => setIsAttachmentPreviewActive(true),
Expand All @@ -80,13 +81,13 @@ function useAttachmentUploadValidation({
);

const attachmentUploadType = useRef<'receipt' | 'attachment'>(undefined);
const onFilesValidated = (files: FileObject[], dataTransferItems: DataTransferItem[]) => {
const onFilesValidated = (files: FileObject[]) => {
if (files.length === 0) {
return;
}

if (attachmentUploadType.current === 'attachment') {
showAttachmentModalScreen(files, dataTransferItems);
showAttachmentModalScreen(files);
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import React, {useEffect, useRef} from 'react';
import type {View} from 'react-native';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
Expand All @@ -7,7 +7,6 @@ import {openReport} from '@libs/actions/Report';
import {getValidatedImageSource} from '@libs/AvatarUtils';
import Navigation from '@libs/Navigation/Navigation';
import {canUserPerformWriteAction, isReportNotFound} from '@libs/ReportUtils';
import validateAttachmentFile from '@libs/validateAttachmentFile';
import type {AttachmentModalBaseContentProps} from '@pages/media/AttachmentModalScreen/AttachmentModalBaseContent/types';
import AttachmentModalContainer from '@pages/media/AttachmentModalScreen/AttachmentModalContainer';
import useDownloadAttachment from '@pages/media/AttachmentModalScreen/routes/hooks/useDownloadAttachment';
Expand All @@ -24,7 +23,7 @@ import AddAttachmentModalCarouselView from './AddAttachmentModalCarouselView';
function ReportAddAttachmentModalContent({route, navigation}: AttachmentModalScreenProps<typeof SCREENS.REPORT_ADD_ATTACHMENT>) {
const {
attachmentID,
file: fileParam,
file,
source: sourceParam,
isAuthTokenRequired,
attachmentLink,
Expand Down Expand Up @@ -53,15 +52,13 @@ function ReportAddAttachmentModalContent({route, navigation}: AttachmentModalScr
const submitRef = useRef<View | HTMLElement>(null);

// Extract the reportActionID from the attachmentID (format: reportActionID_index)
const reportActionID = useMemo(() => attachmentID?.split('_')?.[0], [attachmentID]);
const reportActionID = attachmentID?.split('_')?.[0];

const shouldFetchReport = useMemo(() => {
return isEmptyObject(reportActions?.[reportActionID ?? CONST.DEFAULT_NUMBER_ID]);
}, [reportActions, reportActionID]);
const shouldFetchReport = isEmptyObject(reportActions?.[reportActionID ?? CONST.DEFAULT_NUMBER_ID]);

const fetchReport = useCallback(() => {
const fetchReport = () => {
openReport({reportID, introSelected, reportActionID, betas});
}, [reportID, introSelected, reportActionID, betas]);
};

// Close the modal if user loses write access (e.g., admin switches "Who can post" to Admins only)
useEffect(() => {
Expand All @@ -79,70 +76,41 @@ function ReportAddAttachmentModalContent({route, navigation}: AttachmentModalScr
fetchReport();
}, [reportID, fetchReport, shouldFetchReport]);

const [source, setSource] = useState(() => getValidatedImageSource(sourceParam));
const source = getValidatedImageSource(sourceParam);

const [validFiles, setValidFiles] = useState<FileObject | FileObject[] | undefined>(fileParam);
useEffect(() => {
async function validateFiles() {
if (!fileParam) {
return;
}

const files = Array.isArray(fileParam) ? fileParam : [fileParam];
const results = await Promise.all(files.map(async (file) => validateAttachmentFile(file)));

const validResults = results.filter((r) => r.isValid);
if (validResults.length === 0) {
return;
}
const modalType = useReportAttachmentModalType(source, file);
useNavigateToReportOnRefresh({source: sourceParam, file, reportID});

const validatedFiles = validResults.map((r) => r.file);
const firstValidSource = validResults.at(0)?.file.uri;
let isLoading = false;
const isEmptyReport = isEmptyObject(report);
isLoading =
(!isOffline && !isReportNotFound(report) && !!reportID && !!isLoadingApp) ||
isEmptyReport ||
(reportMetadata?.isLoadingInitialReportActions !== false && shouldFetchReport) ||
(Array.isArray(file) && file.length === 0);

setSource(firstValidSource);
setValidFiles(validatedFiles);
const onConfirm = (f: FileObject | FileObject[]) => {
if (Array.isArray(file) && file.length > 0) {
onConfirmParam?.(file);
} else {
onConfirmParam?.(f);
}

validateFiles();
}, [fileParam]);

const modalType = useReportAttachmentModalType(source, validFiles);
useNavigateToReportOnRefresh({source: sourceParam, file: validFiles, reportID});

const isLoading = useMemo(() => {
if (isOffline || isReportNotFound(report) || !reportID) {
return false;
}
const isEmptyReport = isEmptyObject(report);
return !!isLoadingApp || isEmptyReport || (reportMetadata?.isLoadingInitialReportActions !== false && shouldFetchReport) || (Array.isArray(validFiles) && validFiles.length === 0);
}, [isOffline, report, reportID, isLoadingApp, reportMetadata?.isLoadingInitialReportActions, shouldFetchReport, validFiles]);

const onConfirm = useCallback(
(f: FileObject | FileObject[]) => {
if (Array.isArray(validFiles) && validFiles.length > 0) {
onConfirmParam?.(validFiles);
} else {
onConfirmParam?.(f);
}
},
[validFiles, onConfirmParam],
);
};

const onDownloadAttachment = useDownloadAttachment({
isAuthTokenRequired,
});

useNavigateToReportOnRefresh({source: sourceParam, file: validFiles, reportID});
useNavigateToReportOnRefresh({source: sourceParam, file, reportID});

const contentProps = useMemo<AttachmentModalBaseContentProps>(() => {
if (validFiles === undefined || (Array.isArray(validFiles) && validFiles.length === 0)) {
return {
isLoading: true,
};
}

return {
file: validFiles,
let contentProps: AttachmentModalBaseContentProps;
if (file === undefined || (Array.isArray(file) && file.length === 0)) {
contentProps = {
isLoading: true,
};
} else {
contentProps = {
file,
source,
isLoading,
isAuthTokenRequired,
Expand All @@ -157,20 +125,7 @@ function ReportAddAttachmentModalContent({route, navigation}: AttachmentModalScr
onDownloadAttachment,
AttachmentContent: AddAttachmentModalCarouselView,
};
}, [
validFiles,
source,
isLoading,
isAuthTokenRequired,
attachmentLink,
originalFileName,
attachmentID,
accountID,
headerTitle,
shouldDisableSendButton,
onConfirm,
onDownloadAttachment,
]);
}

return (
<AttachmentModalContainer
Expand Down
Loading
Loading