From cfb4a87dcba99d93fa6b51e54fccf33161a7a387 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 1 Jul 2025 17:32:34 +0700 Subject: [PATCH 01/52] refactor: use attachmentmodal on other screens --- .../Navigation/AppNavigator/AuthScreens.tsx | 34 +++++-------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index d40102010bc1..83b85a821b22 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -31,7 +31,6 @@ import NavBarManager from '@libs/NavBarManager'; import getCurrentUrl from '@libs/Navigation/currentUrl'; import Navigation from '@libs/Navigation/Navigation'; import Animations, {InternalPlatformAnimations} from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation'; -import Presentation from '@libs/Navigation/PlatformStackNavigation/navigationOptions/presentation'; import type {AuthScreensParamList} from '@libs/Navigation/types'; import NetworkConnection from '@libs/NetworkConnection'; import onyxSubscribe from '@libs/onyxSubscribe'; @@ -95,10 +94,6 @@ const loadLogOutPreviousUserPage = () => require('../../.. const loadConciergePage = () => require('../../../pages/ConciergePage').default; const loadTrackExpensePage = () => require('../../../pages/TrackExpensePage').default; const loadSubmitExpensePage = () => require('../../../pages/SubmitExpensePage').default; -const loadProfileAvatar = () => require('../../../pages/settings/Profile/ProfileAvatar').default; -const loadWorkspaceAvatar = () => require('../../../pages/workspace/WorkspaceAvatar').default; -const loadReportAvatar = () => require('../../../pages/ReportAvatar').default; -const loadReceiptView = () => require('../../../pages/TransactionReceiptPage').default; const loadWorkspaceJoinUser = () => require('@pages/workspace/WorkspaceJoinUserPage').default; const loadReportSplitNavigator = () => require('./Navigators/ReportsSplitNavigator').default; @@ -607,30 +602,20 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie /> Date: Tue, 1 Jul 2025 18:01:28 +0700 Subject: [PATCH 02/52] refactor: migrate remaining screens --- src/ROUTES.ts | 8 + src/components/AttachmentModal.tsx | 714 ------------------ src/components/AvatarWithImagePicker.tsx | 214 +++--- src/libs/Navigation/types.ts | 3 - src/pages/ReportAvatar.tsx | 57 -- src/pages/Share/ShareDetailsPage.tsx | 43 +- src/pages/TransactionReceiptPage.tsx | 122 --- .../AttachmentPickerWithMenuItems.tsx | 8 +- .../ComposerWithSuggestions.tsx | 6 +- .../ReportActionCompose.tsx | 252 ++++--- src/pages/settings/Profile/ProfileAvatar.tsx | 51 -- src/pages/workspace/WorkspaceAvatar.tsx | 48 -- 12 files changed, 273 insertions(+), 1253 deletions(-) delete mode 100644 src/components/AttachmentModal.tsx delete mode 100644 src/pages/ReportAvatar.tsx delete mode 100644 src/pages/TransactionReceiptPage.tsx delete mode 100644 src/pages/settings/Profile/ProfileAvatar.tsx delete mode 100644 src/pages/workspace/WorkspaceAvatar.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 15d7874dc603..8fec7ea6c57b 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -407,6 +407,14 @@ const ROUTES = { return getUrlWithBackToParam(`${baseRoute}${queryString}` as const, backTo); }, }, + REPORT_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 getAttachmentModalScreenRoute(`r/${reportID}/attachment/add`, restParams); + }, + }, REPORT_AVATAR: { route: 'r/:reportID/avatar', getRoute: (reportID: string, policyID?: string) => { diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx deleted file mode 100644 index 2bfe11782fc9..000000000000 --- a/src/components/AttachmentModal.tsx +++ /dev/null @@ -1,714 +0,0 @@ -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'; -import {useOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; -import Animated, {FadeIn, LayoutAnimationConfig, 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 type {IOUAction, IOUType} from '@src/CONST'; -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 useAttachmentErrors from './Attachments/AttachmentView/useAttachmentErrors'; -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; - - /** The id of the attachment. */ - attachmentID?: string; - - /** 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 ID of the current report */ - reportID?: string; - - /** 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; - - /** The iou action of the expense creation flow of which we are displaying the receipt for. */ - iouAction?: IOUAction; - - /** The iou type of the expense creation flow of which we are displaying the receipt for. */ - iouType?: IOUType; - - /** The id of the draft transaction linked to the receipt. */ - draftTransactionID?: string; - - fallbackSource?: AvatarSource; - - canEditReceipt?: boolean; - - canDeleteReceipt?: boolean; - - shouldDisableSendButton?: boolean; - - attachmentLink?: string; - - shouldHandleNavigationBack?: boolean; -}; - -function AttachmentModal({ - source = '', - onConfirm, - defaultOpen = false, - originalFileName = '', - isAuthTokenRequired = false, - allowDownload = false, - isTrackExpenseAction = false, - report, - reportID, - onModalShow = () => {}, - onModalHide = () => {}, - onCarouselAttachmentChange = () => {}, - isReceiptAttachment = false, - isWorkspaceAvatar = false, - maybeIcon = false, - headerTitle, - children, - fallbackSource, - canEditReceipt = false, - canDeleteReceipt = false, - onModalClose = () => {}, - isLoading = false, - shouldShowNotFoundPage = false, - type = undefined, - attachmentID, - accountID = undefined, - shouldDisableSendButton = false, - draftTransactionID, - iouAction, - iouType: iouTypeProp, - attachmentLink = '', - shouldHandleNavigationBack, -}: 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 isReplaceReceipt = useRef(false); - const {windowWidth} = useWindowDimensions(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const nope = useSharedValue(false); - const isOverlayModalVisible = (isReceiptAttachment && isDeleteReceiptConfirmModalVisible) || (!isReceiptAttachment && isAttachmentInvalid); - const iouType = useMemo(() => iouTypeProp ?? (isTrackExpenseAction ? CONST.IOU.TYPE.TRACK : CONST.IOU.TYPE.SUBMIT), [isTrackExpenseAction, iouTypeProp]); - 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}`, {canBeMissing: true}); - const [currentAttachmentLink, setCurrentAttachmentLink] = useState(attachmentLink); - const {setAttachmentError, isErrorInAttachment, clearAttachmentErrors} = useAttachmentErrors(); - - 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 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) { - 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 ?? '', undefined, undefined, undefined, undefined, undefined, !draftTransactionID); - } - - // 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, draftTransactionID]); - - /** - * 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) { - // linter keep complain about accessing ref during render - // eslint-disable-next-line react-compiler/react-compiler - menuItems.push({ - icon: Expensicons.Camera, - text: translate('common.replace'), - onSelected: () => { - closeModal(true); - // Set the ref to true, so when the modal is hidden, we will navigate to the scan receipt screen - isReplaceReceipt.current = true; - }, - }); - } - if ((!isOffline && allowDownload && !isLocalSource) || !!draftTransactionID) { - 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]); - - const headerTitleNew = headerTitle ?? translate(isReceiptAttachment ? 'common.receipt' : 'common.attachment'); - const shouldShowThreeDotsButton = isReceiptAttachment && isModalOpen && threeDotsMenuItems.length !== 0; - let shouldShowDownloadButton = false; - if ((!isEmptyObject(report) || type === CONST.ATTACHMENT_TYPE.SEARCH) && !isErrorInAttachment(sourceState)) { - shouldShowDownloadButton = allowDownload && isDownloadButtonReadyToBeShown && !shouldShowNotFoundPage && !isReceiptAttachment && !isOffline && !isLocalSource; - } - const context = useMemo( - () => ({ - pagerItems: [{source: sourceForAttachmentView, index: 0, isActive: true}], - activePage: 0, - pagerRef: undefined, - isPagerScrolling: nope, - isScrollEnabled: nope, - onTap: () => {}, - onScaleChanged: () => {}, - onSwipeDown: closeModal, - onAttachmentError: setAttachmentError, - }), - [closeModal, setAttachmentError, nope, sourceForAttachmentView], - ); - - const submitRef = useRef(null); - - return ( - <> - { - onModalShow(); - setShouldLoadAttachment(true); - }} - onModalHide={() => { - if (!isPDFLoadError.current) { - onModalHide(); - } - setShouldLoadAttachment(false); - clearAttachmentErrors(); - if (isPDFLoadError.current) { - setIsAttachmentInvalid(true); - setAttachmentInvalidReasonTitle('attachmentPicker.attachmentError'); - setAttachmentInvalidReason('attachmentPicker.errorWhileSelectingCorruptedAttachment'); - return; - } - - if (isReplaceReceipt.current) { - InteractionManager.runAfterInteractions(() => { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( - iouAction ?? CONST.IOU.ACTION.EDIT, - iouType, - draftTransactionID ?? transaction?.transactionID, - report?.reportID, - Navigation.getActiveRoute(), - ), - ); - }); - } - }} - propagateSwipe - initialFocus={() => { - if (!submitRef.current) { - return false; - } - return submitRef.current; - }} - shouldHandleNavigationBack={shouldHandleNavigationBack} - > - - {shouldUseNarrowLayout && } - downloadAttachment()} - shouldShowCloseButton={!shouldUseNarrowLayout} - shouldShowBackButton={shouldUseNarrowLayout} - onBackButtonPress={closeModal} - onCloseButtonPress={closeModal} - shouldShowThreeDotsButton={shouldShowThreeDotsButton} - threeDotsAnchorPosition={styles.threeDotsPopoverOffsetAttachmentModal(windowWidth)} - threeDotsAnchorAlignment={{ - horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, - vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, - }} - shouldSetModalVisibility={false} - threeDotsMenuItems={threeDotsMenuItems} - shouldOverlayDots - subTitleLink={currentAttachmentLink ?? ''} - shouldDisplayHelpButton={false} - /> - - {isLoading && } - {shouldShowNotFoundPage && !isLoading && ( - Navigation.dismissModal()} - /> - )} - {!shouldShowNotFoundPage && - !isLoading && - // We shouldn't show carousel arrow in search result attachment - (!isEmptyObject(report) && !isReceiptAttachment && type !== CONST.ATTACHMENT_TYPE.SEARCH ? ( - - ) : ( - !!sourceForAttachmentView && - shouldLoadAttachment && ( - - { - isPDFLoadError.current = true; - closeModal(); - }} - isWorkspaceAvatar={isWorkspaceAvatar} - maybeIcon={maybeIcon} - fallbackSource={fallbackSource} - isUsedInAttachmentModal - transactionID={transaction?.transactionID} - isUploaded={!isEmptyObject(report)} - reportID={reportID ?? (!isEmptyObject(report) ? report.reportID : undefined)} - /> - - ) - ))} - - {/* If we have an onConfirm method show a confirmation button */} - - {!!onConfirm && !isConfirmButtonDisabled && ( - - {({safeAreaPaddingBottomStyle}) => ( - -