diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 38f513fe8ea7..69a27743023c 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -478,6 +478,12 @@ const ROUTES = { route: 'attachment', getRoute: (params?: ReportAttachmentsRouteParams) => getAttachmentModalScreenRoute('attachment', params), }, + REPORT_ADD_ATTACHMENT: { + route: 'r/:reportID/attachment/add', + getRoute: (reportID: string, params?: ReportAddAttachmentRouteParams) => { + return getAttachmentModalScreenRoute(`r/${reportID}/attachment/add`, params); + }, + }, EDIT_CURRENCY_REQUEST: { route: 'r/:threadReportID/edit/currency', getRoute: (threadReportID: string, currency: string, backTo: string) => `r/${threadReportID}/edit/currency?currency=${currency}&backTo=${backTo}` as const, @@ -3281,11 +3287,13 @@ export {PUBLIC_SCREENS_ROUTES, SHARED_ROUTE_PARAMS, VERIFY_ACCOUNT}; export default ROUTES; type ReportAttachmentsRoute = typeof ROUTES.ATTACHMENTS.route; -type AttachmentRoutes = ReportAttachmentsRoute; +type ReportAddAttachmentRoute = `r/${string}/attachment/add`; +type AttachmentRoutes = ReportAttachmentsRoute | ReportAddAttachmentRoute; type ReportAttachmentsRouteParams = RootNavigatorParamList[typeof SCREENS.ATTACHMENTS]; +type ReportAddAttachmentRouteParams = RootNavigatorParamList[typeof SCREENS.REPORT_ADD_ATTACHMENT]; -function getAttachmentModalScreenRoute(url: AttachmentRoutes, params?: ReportAttachmentsRouteParams) { +function getAttachmentModalScreenRoute(url: AttachmentRoutes, params?: ReportAttachmentsRouteParams | ReportAddAttachmentRouteParams) { if (!params?.source) { return url; } diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 090fae7a7487..c2dbfe7883da 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -8,6 +8,7 @@ const PROTECTED_SCREENS = { HOME: 'Home', CONCIERGE: 'Concierge', ATTACHMENTS: 'Attachments', + REPORT_ADD_ATTACHMENT: 'ReportAddAttachment', TRACK_EXPENSE: 'TrackExpense', SUBMIT_EXPENSE: 'SubmitExpense', } as const; diff --git a/src/components/AttachmentComposerModal.tsx b/src/components/AttachmentComposerModal.tsx deleted file mode 100644 index 35bcfb10b4d7..000000000000 --- a/src/components/AttachmentComposerModal.tsx +++ /dev/null @@ -1,303 +0,0 @@ -import React, {memo, useCallback, useEffect, useRef, useState} from 'react'; -import type {View} 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 useFilesValidation from '@hooks/useFilesValidation'; -import useLocalize from '@hooks/useLocalize'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useThemeStyles from '@hooks/useThemeStyles'; -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'; -import viewRef from '@src/types/utils/viewRef'; -import AttachmentCarouselView from './Attachments/AttachmentCarousel/AttachmentCarouselView'; -import useCarouselArrows from './Attachments/AttachmentCarousel/useCarouselArrows'; -import useAttachmentErrors from './Attachments/AttachmentView/useAttachmentErrors'; -import type {Attachment} from './Attachments/types'; -import Button from './Button'; -import HeaderGap from './HeaderGap'; -import HeaderWithBackButton from './HeaderWithBackButton'; -import Modal from './Modal'; -import SafeAreaConsumer from './SafeAreaConsumer'; - -type ImagePickerResponse = { - height?: number; - name: string; - size?: number | null; - type: string; - uri: string; - width?: number; -}; - -type FileObject = Partial; - -type ChildrenProps = { - displayFilesInModal: (data: FileObject[], items?: DataTransferItem[]) => void; - show: () => void; -}; - -type AttachmentComposerModalProps = { - /** Optional callback to fire when we want to preview an image and approve it for use. */ - onConfirm: ((file: FileObject | FileObject[]) => void) | null; - - /** Title shown in the header of the modal */ - headerTitle: string; - - /** Optional callback to fire when we want to do something after modal show. */ - onModalShow: () => void; - - /** Optional callback to fire when we want to do something after modal hide. */ - onModalHide: () => void; - - /** A function as a child to pass modal launching methods to */ - children: React.FC; - - /** Should disable send button */ - shouldDisableSendButton: boolean; - - /** The report currently being looked at */ - report?: OnyxEntry; -}; - -function AttachmentComposerModal({onConfirm, onModalShow = () => {}, onModalHide = () => {}, headerTitle, children, shouldDisableSendButton = false, report}: AttachmentComposerModalProps) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const {setAttachmentError, clearAttachmentErrors} = useAttachmentErrors(); - const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows(); - - const [isModalOpen, setIsModalOpen] = useState(false); - const [modalType, setModalType] = useState(CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE); - const [validFilesToUpload, setValidFilesToUpload] = useState([]); - const [attachments, setAttachments] = useState([]); - const [page, setPage] = useState(0); - const [currentAttachment, setCurrentAttachment] = useState(null); - - /** - * If our attachment is a PDF, return the unswipeable Modal type. - */ - const getModalType = useCallback( - (sourceURL: string, fileObject: FileObject) => { - const fileName = fileObject?.name ?? translate('attachmentView.unknownFilename'); - return sourceURL && (sourceURL.includes('.pdf') || fileName.includes('.pdf')) ? CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE : CONST.MODAL.MODAL_TYPE.CENTERED; - }, - [translate], - ); - - /** - * Execute the onConfirm callback and close the modal. - */ - const submitAndClose = useCallback(() => { - // If the modal has already been closed - if (!isModalOpen) { - return; - } - - if (onConfirm) { - if (validFilesToUpload.length) { - onConfirm(validFilesToUpload); - } else if (currentAttachment) { - onConfirm(currentAttachment.file ?? (currentAttachment as FileObject)); - } - } - - setIsModalOpen(false); - }, [isModalOpen, onConfirm, validFilesToUpload, currentAttachment]); - - /** - * Sanitizes file names and ensures proper URI references for file system compatibility - */ - const cleanFileObjectName = useCallback((fileObject: FileObject): FileObject => { - if (fileObject instanceof File) { - const cleanName = cleanFileName(fileObject.name); - if (fileObject.name !== cleanName) { - const updatedFile = new File([fileObject], cleanName, {type: fileObject.type}); - const inputSource = URL.createObjectURL(updatedFile); - updatedFile.uri = inputSource; - return updatedFile; - } - if (!fileObject.uri) { - const inputSource = URL.createObjectURL(fileObject); - // eslint-disable-next-line no-param-reassign - fileObject.uri = inputSource; - } - } - return fileObject; - }, []); - - const convertFileToAttachment = useCallback((fileObject: FileObject, source: string): Attachment => { - return { - source, - file: fileObject, - }; - }, []); - - useEffect(() => { - if (!validFilesToUpload.length) { - return; - } - - if (validFilesToUpload.length > 0) { - // Convert all files to attachments - const newAttachments = validFilesToUpload.map((fileObject) => { - const source = fileObject.uri ?? ''; - return convertFileToAttachment(fileObject, source); - }); - - const firstAttachment = newAttachments.at(0) ?? null; - setAttachments(newAttachments); - setCurrentAttachment(firstAttachment); - setPage(0); - - if (firstAttachment?.file) { - const inputModalType = getModalType(firstAttachment.source as string, firstAttachment.file); - setModalType(inputModalType); - } - - setIsModalOpen(true); - } - }, [validFilesToUpload, convertFileToAttachment, getModalType]); - - const {ErrorModal, validateFiles, PDFValidationComponent} = useFilesValidation(setValidFilesToUpload, false); - - const validateAndDisplayMultipleFilesToUpload = useCallback( - (data: FileObject[], items?: DataTransferItem[]) => { - if (!data?.length) { - return; - } - - const validIndices: number[] = []; - const fileObjects = data - .map((item, index) => { - let fileObject = item; - if ('getAsFile' in item && typeof item.getAsFile === 'function') { - fileObject = item.getAsFile() as FileObject; - } - const cleanedFileObject = cleanFileObjectName(fileObject); - if (cleanedFileObject !== null) { - validIndices.push(index); - } - return cleanedFileObject; - }) - .filter((fileObject): fileObject is FileObject => fileObject !== null); - - if (!fileObjects.length) { - return; - } - - // 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, validateFiles], - ); - - const closeModal = useCallback(() => { - setIsModalOpen(false); - }, []); - - const openModal = useCallback(() => { - setIsModalOpen(true); - }, []); - - const headerTitleNew = headerTitle ?? translate('reportActionCompose.sendAttachment'); - - const submitRef = useRef(null); - - return ( - <> - {PDFValidationComponent} - { - onModalShow(); - }} - onModalHide={() => { - onModalHide(); - clearAttachmentErrors(); - setValidFilesToUpload([]); - setAttachments([]); - setCurrentAttachment(null); - setPage(0); - }} - initialFocus={() => { - if (!submitRef.current) { - return false; - } - return submitRef.current; - }} - shouldHandleNavigationBack - > - - {shouldUseNarrowLayout && } - - {attachments.length > 0 && !!currentAttachment && ( - - )} - - {(validFilesToUpload.length > 0 || !!currentAttachment) && ( - - {({safeAreaPaddingBottomStyle}) => ( - -