Skip to content
Merged
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
96 changes: 71 additions & 25 deletions src/hooks/useFilesValidation.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Str} from 'expensify-common';
import React, {useRef, useState} from 'react';
import React, {useEffect, useRef, useState} from 'react';
import {InteractionManager} from 'react-native';
import type {ValueOf} from 'type-fest';
import ConfirmModal from '@components/ConfirmModal';
Expand All @@ -18,6 +18,7 @@ import useLocalize from './useLocalize';
import useThemeStyles from './useThemeStyles';

const DEFAULT_IS_VALIDATING_RECEIPTS = true;
const MIN_LOADER_VISIBLE_DURATION_MS = 200;

type ValidationOptions = {
isValidatingReceipts?: boolean;
Expand Down Expand Up @@ -56,6 +57,7 @@ function useFilesValidation(onFilesValidated: (files: FileObject[], dataTransfer
const dataTransferItemList = useRef<DataTransferItem[]>([]);
const collectedErrors = useRef<FileValidationError[]>([]);
const originalFileOrder = useRef<Map<string, number>>(new Map());
const loaderTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);

const updateFileOrderMapping = (oldFile: FileObject | undefined, newFile: FileObject) => {
const originalIndex = originalFileOrder.current.get(oldFile?.uri ?? '');
Expand All @@ -76,6 +78,16 @@ function useFilesValidation(onFilesValidated: (files: FileObject[], dataTransfer
});
};

useEffect(() => {
return () => {
if (!loaderTimeoutRef.current) {
return;
}
clearTimeout(loaderTimeoutRef.current);
loaderTimeoutRef.current = undefined;
};
}, []);

const reset = () => {
setIsValidatingFiles(false);
setIsValidatingReceipts(undefined);
Expand Down Expand Up @@ -141,6 +153,14 @@ function useFilesValidation(onFilesValidated: (files: FileObject[], dataTransfer
return;
}

let loaderStartTime: number | undefined;
const showLoader = () => {
if (loaderStartTime === undefined) {
loaderStartTime = Date.now();
}
setIsLoaderVisible(true);
};

// Reset collected errors for new validation
collectedErrors.current = [];

Expand Down Expand Up @@ -186,7 +206,7 @@ function useFilesValidation(onFilesValidated: (files: FileObject[], dataTransfer
);

if (filesToConvert.length > 0) {
setIsLoaderVisible(true);
showLoader();

const convertedFilesToResize: FileObject[] = [];
const convertedFiles: FileObject[] = [];
Expand Down Expand Up @@ -233,7 +253,7 @@ function useFilesValidation(onFilesValidated: (files: FileObject[], dataTransfer
}

if (filesToResize.length > 0) {
setIsLoaderVisible(true);
showLoader();

const toResizeResults = await Promise.allSettled(filesToResize.map((file) => resizeImageIfNeeded(file)));

Expand All @@ -253,32 +273,58 @@ function useFilesValidation(onFilesValidated: (files: FileObject[], dataTransfer
}
}

setIsLoaderVisible(false);
const handleNext = () => {
if (pdfsToLoad.length) {
validFiles.current = validNonPdfFiles;
setPdfFilesToRender(pdfsToLoad);
return;
}

if (pdfsToLoad.length) {
validFiles.current = validNonPdfFiles;
setPdfFilesToRender(pdfsToLoad);
return;
}
if (validNonPdfFiles.length > 0) {
setValidFilesToUpload(validNonPdfFiles);
}

if (validNonPdfFiles.length > 0) {
setValidFilesToUpload(validNonPdfFiles);
}
if (collectedErrors.current.length > 0) {
const uniqueErrors = deduplicateErrors(collectedErrors.current);
setErrorQueue(uniqueErrors);
setCurrentErrorIndex(0);
const firstError = uniqueErrors.at(0);
if (firstError) {
setFileError(firstError);
setIsErrorModalVisible(true);
}
} else if (validNonPdfFiles.length > 0) {
const sortedFiles = sortFilesByOriginalOrder(validNonPdfFiles, originalFileOrder.current);
onFilesValidated(sortedFiles, dataTransferItemList.current);
reset();
}
};

if (collectedErrors.current.length > 0) {
const uniqueErrors = Array.from(new Set(collectedErrors.current.map((error) => JSON.stringify(error)))).map((errorStr) => JSON.parse(errorStr) as FileValidationError);
setErrorQueue(uniqueErrors);
setCurrentErrorIndex(0);
const firstError = uniqueErrors.at(0);
if (firstError) {
setFileError(firstError);
setIsErrorModalVisible(true);
const hideLoaderAndHandleNext = () => {
setIsLoaderVisible(false);
handleNext();
};

const extendLoaderIfNeeded = () => {
if (loaderStartTime === undefined) {
hideLoaderAndHandleNext();
return;
}
} else if (validNonPdfFiles.length > 0) {
const sortedFiles = sortFilesByOriginalOrder(validNonPdfFiles, originalFileOrder.current);
onFilesValidated(sortedFiles, dataTransferItemList.current);
reset();
}

Comment thread
nkdengineer marked this conversation as resolved.
const elapsedTime = Date.now() - loaderStartTime;
const shouldDelayHide = collectedErrors.current.length > 0 && elapsedTime < MIN_LOADER_VISIBLE_DURATION_MS;

if (!shouldDelayHide) {
hideLoaderAndHandleNext();
return;
}

Comment thread
nkdengineer marked this conversation as resolved.
loaderTimeoutRef.current = setTimeout(() => {
hideLoaderAndHandleNext();
}, MIN_LOADER_VISIBLE_DURATION_MS - elapsedTime);

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.

@dangrous can you please confirm if the value of 200 for MIN_LOADER_VISIBLE_DURATION_MS is enough?

Screen.Recording.2026-04-16.at.16.44.35.mov

};

extendLoaderIfNeeded();
}

const validateFiles = (files: FileObject[], items?: DataTransferItem[], validationOptions?: ValidationOptions) => {
Expand Down
Loading