diff --git a/src/App.tsx b/src/App.tsx index 8dd2631a6b7d..b24f5ddf962f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -37,7 +37,7 @@ import Expensify from './Expensify'; import {CurrentReportIDContextProvider} from './hooks/useCurrentReportID'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; -import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; +import {AttachmentModalProvider} from './pages/media/AttachmentModalScreen/AttachmentModalContext'; import type {Route} from './ROUTES'; import './setup/backgroundTask'; import {SplashScreenStateContextProvider} from './SplashScreenStateContext'; @@ -95,7 +95,7 @@ function App({url, hybridAppSettings, timestamp}: AppProps) { PopoverContextProvider, CurrentReportIDContextProvider, ScrollOffsetContextProvider, - ReportAttachmentsProvider, + AttachmentModalProvider, PickerStateProvider, EnvironmentProvider, CustomStatusBarAndBackgroundContextProvider, diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 41fd4b830f12..24c658167746 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -4,7 +4,9 @@ import type CONST from './CONST'; import type {IOUAction, IOUType} from './CONST'; import type {IOURequestType} from './libs/actions/IOU'; import Log from './libs/Log'; +import type {ReportsSplitNavigatorParamList} from './libs/Navigation/types'; import type {ReimbursementAccountStepToOpen} from './libs/ReimbursementAccountUtils'; +import type SCREENS from './SCREENS'; import type {ExitReason} from './types/form/ExitSurveyReasonForm'; import type {ConnectionName, SageIntacctMappingName} from './types/onyx/Policy'; import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual'; @@ -19,6 +21,25 @@ function getUrlWithBackToParam(url: TUrl, backTo?: string, return `${url}${backToParam}` as `${TUrl}`; } +type AttachmentRouteParams = ReportsSplitNavigatorParamList[typeof SCREENS.ATTACHMENTS]; +function getAttachmentRoute(url: string, params?: AttachmentRouteParams) { + if (!params?.source) { + return url; + } + + const {source, type, reportID, accountID, isAuthTokenRequired, fileName, attachmentLink} = params; + + const sourceParam = `?source=${encodeURIComponent(source)}`; + const typeParam = type ? `&type=${type as string}` : ''; + const reportIDParam = reportID ? `&reportID=${reportID}` : ''; + const accountIDParam = accountID ? `&accountID=${accountID}` : ''; + const authTokenParam = isAuthTokenRequired ? '&isAuthTokenRequired=true' : ''; + const fileNameParam = fileName ? `&fileName=${fileName}` : ''; + const attachmentLinkParam = attachmentLink ? `&attachmentLink=${attachmentLink}` : ''; + + return `${url}${sourceParam}${typeParam}${reportIDParam}${accountIDParam}${authTokenParam}${fileNameParam}${attachmentLinkParam}` as const; +} + const PUBLIC_SCREENS_ROUTES = { // If the user opens this route, we'll redirect them to the path saved in the last visited path or to the home page if the last visited path is empty. ROOT: '', @@ -326,6 +347,14 @@ const ROUTES = { return `${baseRoute}${referrerParam}` as const; }, }, + REPORT_WITH_ID_ADD_ATTACHMENT: { + route: 'r/:reportID/attachment/add', + getRoute: (reportID: string, params?: AttachmentRouteParams) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const {reportID: _reportIDParam, ...restParams} = params ?? {}; + return getAttachmentRoute(`r/${reportID}/attachment/add`, restParams); + }, + }, REPORT_AVATAR: { route: 'r/:reportID/avatar', getRoute: (reportID: string, policyID?: string) => { @@ -362,23 +391,7 @@ const ROUTES = { }, ATTACHMENTS: { route: 'attachment', - getRoute: ( - reportID: string | undefined, - type: ValueOf, - url: string, - accountID?: number, - isAuthTokenRequired?: boolean, - fileName?: string, - attachmentLink?: string, - ) => { - const reportParam = reportID ? `&reportID=${reportID}` : ''; - const accountParam = accountID ? `&accountID=${accountID}` : ''; - const authTokenParam = isAuthTokenRequired ? '&isAuthTokenRequired=true' : ''; - const fileNameParam = fileName ? `&fileName=${fileName}` : ''; - const attachmentLinkParam = attachmentLink ? `&attachmentLink=${attachmentLink}` : ''; - - return `attachment?source=${encodeURIComponent(url)}&type=${type as string}${reportParam}${accountParam}${authTokenParam}${fileNameParam}${attachmentLinkParam}` as const; - }, + getRoute: (params?: AttachmentRouteParams) => getAttachmentRoute('attachment', params), }, REPORT_PARTICIPANTS: { route: 'r/:reportID/participants', @@ -398,7 +411,7 @@ const ROUTES = { }, REPORT_WITH_ID_DETAILS: { route: 'r/:reportID/details', - getRoute: (reportID: string | undefined, backTo?: string) => { + getRoute: (reportID: string | number | undefined, backTo?: string) => { if (!reportID) { Log.warn('Invalid reportID is used to build the REPORT_WITH_ID_DETAILS route'); } diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx deleted file mode 100644 index 4373dae8ac73..000000000000 --- a/src/components/AttachmentModal.tsx +++ /dev/null @@ -1,661 +0,0 @@ -import {Str} from 'expensify-common'; -import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {Keyboard, View} from 'react-native'; -import {GestureHandlerRootView} from 'react-native-gesture-handler'; -import {useOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; -import Animated, {FadeIn, useSharedValue} from 'react-native-reanimated'; -import type {ValueOf} from 'type-fest'; -import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useThemeStyles from '@hooks/useThemeStyles'; -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 Navigation from '@libs/Navigation/Navigation'; -import {getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import {hasEReceipt, hasMissingSmartscanFields, hasReceipt, hasReceiptSource, isReceiptBeingScanned} from '@libs/TransactionUtils'; -import type {AvatarSource} from '@libs/UserUtils'; -import variables from '@styles/variables'; -import {detachReceipt} from '@userActions/IOU'; -import CONST from '@src/CONST'; -import type {TranslationPaths} from '@src/languages/types'; -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'; -import AttachmentView from './Attachments/AttachmentView'; -import type {Attachment} from './Attachments/types'; -import BlockingView from './BlockingViews/BlockingView'; -import Button from './Button'; -import ConfirmModal from './ConfirmModal'; -import FullScreenLoadingIndicator from './FullscreenLoadingIndicator'; -import HeaderGap from './HeaderGap'; -import HeaderWithBackButton from './HeaderWithBackButton'; -import * as Expensicons from './Icon/Expensicons'; -import * as Illustrations from './Icon/Illustrations'; -import Modal from './Modal'; -import SafeAreaConsumer from './SafeAreaConsumer'; - -/** - * Modal render prop component that exposes modal launching triggers that can be used - * to display a full size image or PDF modally with optional confirmation button. - */ - -type ImagePickerResponse = { - height?: number; - name: string; - size?: number | null; - type: string; - uri: string; - width?: number; -}; - -type FileObject = Partial; - -type ChildrenProps = { - displayFileInModal: (data: FileObject) => void; - show: () => void; -}; - -type AttachmentModalProps = { - /** Optional source (URL, SVG function) for the image shown. If not passed in via props must be specified when modal is opened. */ - source?: AvatarSource; - - /** Optional callback to fire when we want to preview an image and approve it for use. */ - onConfirm?: ((file: FileObject) => void) | null; - - /** Whether the modal should be open by default */ - defaultOpen?: boolean; - - /** Trigger when we explicity click close button in ProfileAttachment modal */ - onModalClose?: () => void; - - /** Optional original filename when uploading */ - originalFileName?: string; - - /** Whether source url requires authentication */ - isAuthTokenRequired?: boolean; - - /** Determines if download Button should be shown or not */ - allowDownload?: boolean; - - /** Determines if the receipt comes from track expense action */ - isTrackExpenseAction?: boolean; - - /** Title shown in the header of the modal */ - headerTitle?: string; - - /** The report that has this attachment */ - report?: OnyxEntry; - - /** The type of the attachment */ - type?: ValueOf; - - /** If the attachment originates from a note, the accountID will represent the author of that note. */ - accountID?: number; - - /** 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; - - /** The data is loading or not */ - isLoading?: boolean; - - /** Should display not found page or not */ - shouldShowNotFoundPage?: boolean; - - /** Optional callback to fire when we want to do something after attachment carousel changes. */ - onCarouselAttachmentChange?: (attachment: Attachment) => void; - - /** Denotes whether it is a workspace avatar or not */ - isWorkspaceAvatar?: boolean; - - /** Denotes whether it can be an icon (ex: SVG) */ - maybeIcon?: boolean; - - /** Whether it is a receipt attachment or not */ - isReceiptAttachment?: boolean; - - /** A function as a child to pass modal launching methods to */ - children?: React.FC; - - fallbackSource?: AvatarSource; - - canEditReceipt?: boolean; - - canDeleteReceipt?: boolean; - - shouldDisableSendButton?: boolean; - - attachmentLink?: string; -}; - -function AttachmentModal({ - source = '', - onConfirm, - defaultOpen = false, - originalFileName = '', - isAuthTokenRequired = false, - allowDownload = false, - isTrackExpenseAction = false, - report, - onModalShow = () => {}, - onModalHide = () => {}, - onCarouselAttachmentChange = () => {}, - isReceiptAttachment = false, - isWorkspaceAvatar = false, - maybeIcon = false, - headerTitle, - children, - fallbackSource, - canEditReceipt = false, - canDeleteReceipt = false, - onModalClose = () => {}, - isLoading = false, - shouldShowNotFoundPage = false, - type = undefined, - accountID = undefined, - shouldDisableSendButton = false, - attachmentLink = '', -}: AttachmentModalProps) { - const styles = useThemeStyles(); - const [isModalOpen, setIsModalOpen] = useState(defaultOpen); - const [shouldLoadAttachment, setShouldLoadAttachment] = useState(false); - const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); - const [isDeleteReceiptConfirmModalVisible, setIsDeleteReceiptConfirmModalVisible] = useState(false); - const [isAuthTokenRequiredState, setIsAuthTokenRequiredState] = useState(isAuthTokenRequired); - const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState(null); - const [attachmentInvalidReason, setAttachmentInvalidReason] = useState(null); - const [sourceState, setSourceState] = useState(() => source); - const [modalType, setModalType] = useState(CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE); - const [isConfirmButtonDisabled, setIsConfirmButtonDisabled] = useState(false); - const [isDownloadButtonReadyToBeShown, setIsDownloadButtonReadyToBeShown] = React.useState(true); - const isPDFLoadError = useRef(false); - const {windowWidth} = useWindowDimensions(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const nope = useSharedValue(false); - const isOverlayModalVisible = (isReceiptAttachment && isDeleteReceiptConfirmModalVisible) || (!isReceiptAttachment && isAttachmentInvalid); - const iouType = useMemo(() => (isTrackExpenseAction ? CONST.IOU.TYPE.TRACK : CONST.IOU.TYPE.SUBMIT), [isTrackExpenseAction]); - const parentReportAction = getReportAction(report?.parentReportID, report?.parentReportActionID); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const transactionID = (isMoneyRequestAction(parentReportAction) && getOriginalMessage(parentReportAction)?.IOUTransactionID) || CONST.DEFAULT_NUMBER_ID; - const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); - const [currentAttachmentLink, setCurrentAttachmentLink] = useState(attachmentLink); - - const [file, setFile] = useState( - originalFileName - ? { - name: originalFileName, - } - : undefined, - ); - const {translate} = useLocalize(); - const {isOffline} = useNetwork(); - - const isLocalSource = typeof sourceState === 'string' && /^file:|^blob:/.test(sourceState); - - useEffect(() => { - setFile(originalFileName ? {name: originalFileName} : undefined); - }, [originalFileName]); - - /** - * Keeps the attachment source in sync with the attachment displayed currently in the carousel. - */ - const onNavigate = useCallback( - (attachment: Attachment) => { - setSourceState(attachment.source); - setFile(attachment.file); - setIsAuthTokenRequiredState(attachment.isAuthTokenRequired ?? false); - onCarouselAttachmentChange(attachment); - setCurrentAttachmentLink(attachment?.attachmentLink ?? ''); - }, - [onCarouselAttachmentChange], - ); - - /** - * If our attachment is a PDF, return the unswipeablge 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) { - return; - } - setIsDownloadButtonReadyToBeShown(isButtonVisible); - }, - [isDownloadButtonReadyToBeShown], - ); - - /** - * Download the currently viewed attachment. - */ - const downloadAttachment = useCallback(() => { - let sourceURL = sourceState; - if (isAuthTokenRequiredState && typeof sourceURL === 'string') { - sourceURL = addEncryptedAuthTokenToURL(sourceURL); - } - - if (typeof sourceURL === 'string') { - const fileName = type === CONST.ATTACHMENT_TYPE.SEARCH ? getFileName(`${sourceURL}`) : file?.name; - fileDownload(sourceURL, fileName ?? ''); - } - - // At ios, if the keyboard is open while opening the attachment, then after downloading - // the attachment keyboard will show up. So, to fix it we need to dismiss the keyboard. - Keyboard.dismiss(); - }, [isAuthTokenRequiredState, sourceState, file, type]); - - /** - * Execute the onConfirm callback and close the modal. - */ - const submitAndClose = useCallback(() => { - // If the modal has already been closed or the confirm button is disabled - // do not submit. - if (!isModalOpen || isConfirmButtonDisabled) { - return; - } - - if (onConfirm) { - onConfirm(Object.assign(file ?? {}, {source: sourceState} as FileObject)); - } - - setIsModalOpen(false); - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [isModalOpen, isConfirmButtonDisabled, onConfirm, file, sourceState]); - - /** - * Close the confirm modals. - */ - const closeConfirmModal = useCallback(() => { - setIsAttachmentInvalid(false); - setIsDeleteReceiptConfirmModalVisible(false); - }, []); - - /** - * Detach the receipt and close the modal. - */ - const deleteAndCloseModal = useCallback(() => { - detachReceipt(transaction?.transactionID); - setIsDeleteReceiptConfirmModalVisible(false); - 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`. - * This is useful when you plan to continue navigating to another page after closing the modal, to avoid freezing the app due to navigating to another page first and dismissing the modal later. - * If `shouldCallDirectly` is false or undefined, it calls `attachmentModalHandler.handleModalClose` to close the modal. - * This ensures smooth modal closing behavior without causing delays in closing. - */ - const closeModal = useCallback( - (shouldCallDirectly?: boolean) => { - setIsModalOpen(false); - - if (typeof onModalClose === 'function') { - if (shouldCallDirectly) { - onModalClose(); - return; - } - attachmentModalHandler.handleModalClose(onModalClose); - } - - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, - [onModalClose], - ); - - /** - * open the modal - */ - const openModal = useCallback(() => { - setIsModalOpen(true); - }, []); - - useEffect(() => { - setSourceState(() => source); - }, [source]); - - useEffect(() => { - setIsAuthTokenRequiredState(isAuthTokenRequired); - }, [isAuthTokenRequired]); - - const sourceForAttachmentView = sourceState || source; - - const threeDotsMenuItems = useMemo(() => { - if (!isReceiptAttachment) { - return []; - } - - const menuItems = []; - if (canEditReceipt) { - menuItems.push({ - icon: Expensicons.Camera, - text: translate('common.replace'), - onSelected: () => { - closeModal(true); - Navigation.isNavigationReady().then(() => { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID, report?.reportID, Navigation.getActiveRouteWithoutParams()), - ); - }); - }, - }); - } - if (!isOffline && allowDownload && !isLocalSource) { - menuItems.push({ - icon: Expensicons.Download, - text: translate('common.download'), - onSelected: () => downloadAttachment(), - }); - } - - const hasOnlyEReceipt = hasEReceipt(transaction) && !hasReceiptSource(transaction); - if (!hasOnlyEReceipt && hasReceipt(transaction) && !isReceiptBeingScanned(transaction) && canDeleteReceipt && !hasMissingSmartscanFields(transaction)) { - menuItems.push({ - icon: Expensicons.Trashcan, - text: translate('receipt.deleteReceipt'), - onSelected: () => { - setIsDeleteReceiptConfirmModalVisible(true); - }, - shouldCallAfterModalHide: true, - }); - } - return menuItems; - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [isReceiptAttachment, transaction, file, sourceState, iouType]); - - // There are a few things that shouldn't be set until we absolutely know if the file is a receipt or an attachment. - // props.isReceiptAttachment will be null until its certain what the file is, in which case it will then be true|false. - let headerTitleNew = headerTitle; - let shouldShowDownloadButton = false; - let shouldShowThreeDotsButton = false; - if (!isEmptyObject(report) || type === CONST.ATTACHMENT_TYPE.SEARCH) { - headerTitleNew = translate(isReceiptAttachment ? 'common.receipt' : 'common.attachment'); - shouldShowDownloadButton = allowDownload && isDownloadButtonReadyToBeShown && !shouldShowNotFoundPage && !isReceiptAttachment && !isOffline && !isLocalSource; - shouldShowThreeDotsButton = isReceiptAttachment && isModalOpen && threeDotsMenuItems.length !== 0; - } - const context = useMemo( - () => ({ - pagerItems: [{source: sourceForAttachmentView, index: 0, isActive: true}], - activePage: 0, - pagerRef: undefined, - isPagerScrolling: nope, - isScrollEnabled: nope, - onTap: () => {}, - onScaleChanged: () => {}, - onSwipeDown: closeModal, - }), - [closeModal, nope, sourceForAttachmentView], - ); - - const submitRef = useRef(null); - - return ( - <> - { - onModalShow(); - setShouldLoadAttachment(true); - }} - onModalHide={() => { - if (!isPDFLoadError.current) { - onModalHide(); - } - setShouldLoadAttachment(false); - if (isPDFLoadError.current) { - setIsAttachmentInvalid(true); - setAttachmentInvalidReasonTitle('attachmentPicker.attachmentError'); - setAttachmentInvalidReason('attachmentPicker.errorWhileSelectingCorruptedAttachment'); - } - }} - propagateSwipe - initialFocus={() => { - if (!submitRef.current) { - return false; - } - return submitRef.current; - }} - > - - {shouldUseNarrowLayout && } - downloadAttachment()} - shouldShowCloseButton={!shouldUseNarrowLayout} - shouldShowBackButton={shouldUseNarrowLayout} - onBackButtonPress={closeModal} - onCloseButtonPress={closeModal} - shouldShowThreeDotsButton={shouldShowThreeDotsButton} - threeDotsAnchorPosition={styles.threeDotsPopoverOffsetAttachmentModal(windowWidth)} - threeDotsMenuItems={threeDotsMenuItems} - shouldOverlayDots - subTitleLink={currentAttachmentLink ?? ''} - /> - - {isLoading && } - {shouldShowNotFoundPage && !isLoading && ( - Navigation.dismissModal()} - /> - )} - {!shouldShowNotFoundPage && - (!isEmptyObject(report) && !isReceiptAttachment ? ( - - ) : ( - !!sourceForAttachmentView && - shouldLoadAttachment && - !isLoading && ( - - { - isPDFLoadError.current = true; - closeModal(); - }} - isWorkspaceAvatar={isWorkspaceAvatar} - maybeIcon={maybeIcon} - fallbackSource={fallbackSource} - isUsedInAttachmentModal - transactionID={transaction?.transactionID} - isUploaded={!isEmptyObject(report)} - /> - - ) - ))} - - {/* If we have an onConfirm method show a confirmation button */} - {!!onConfirm && !isConfirmButtonDisabled && ( - - {({safeAreaPaddingBottomStyle}) => ( - -