diff --git a/src/App.tsx b/src/App.tsx index e65fa34323c14..dc17fe242a550 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -41,7 +41,7 @@ import {CurrentReportIDContextProvider} from './hooks/useCurrentReportID'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; import HybridAppHandler from './HybridAppHandler'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; -import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; +import {AttachmentModalContextProvider} from './pages/media/AttachmentModalScreen/AttachmentModalContext'; import type {Route} from './ROUTES'; import './setup/backgroundTask'; import './setup/hybridApp'; @@ -95,7 +95,7 @@ function App({url, hybridAppSettings}: AppProps) { PopoverContextProvider, CurrentReportIDContextProvider, ScrollOffsetContextProvider, - ReportAttachmentsProvider, + AttachmentModalContextProvider, PickerStateProvider, EnvironmentProvider, CustomStatusBarAndBackgroundContextProvider, diff --git a/src/ROUTES.ts b/src/ROUTES.ts index b005439ad4e6e..543691c0b3a43 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -5,6 +5,7 @@ import type {IOUAction, IOUType} from './CONST'; import type {IOURequestType} from './libs/actions/IOU'; import Log from './libs/Log'; import type {ReimbursementAccountStepToOpen} from './libs/ReimbursementAccountUtils'; +import type {AttachmentModalScreenParams} from './pages/media/AttachmentModalScreen/types'; import SCREENS from './SCREENS'; import type {Screen} from './SCREENS'; import type {ExitReason} from './types/form/ExitSurveyReasonForm'; @@ -443,28 +444,7 @@ const ROUTES = { }, ATTACHMENTS: { route: 'attachment', - getRoute: ( - reportID: string | undefined, - attachmentID: string | undefined, - type: ValueOf | undefined, - url: string, - accountID?: number, - isAuthTokenRequired?: boolean, - fileName?: string, - attachmentLink?: string, - hashKey?: number, - ) => { - const typeParam = type ? `&type=${type}` : ''; - 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}` : ''; - const attachmentIDParam = attachmentID ? `&attachmentID=${attachmentID}` : ''; - const hashKeyParam = hashKey ? `&hashKey=${hashKey}` : ''; - - return `attachment?source=${encodeURIComponent(url)}${typeParam}${reportParam}${attachmentIDParam}${accountParam}${authTokenParam}${fileNameParam}${attachmentLinkParam}${hashKeyParam}` as const; - }, + getRoute: (params?: AttachmentRouteParams) => getAttachmentModalScreenRoute('attachment', params), }, REPORT_PARTICIPANTS: { route: 'r/:reportID/participants', @@ -484,7 +464,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'); } @@ -2634,6 +2614,30 @@ const SHARED_ROUTE_PARAMS: Partial> = { export {HYBRID_APP_ROUTES, getUrlWithBackToParam, PUBLIC_SCREENS_ROUTES, SHARED_ROUTE_PARAMS}; export default ROUTES; +type AttachmentsRoute = typeof ROUTES.ATTACHMENTS.route; +type ReportAddAttachmentRoute = `r/${string}/attachment/add`; +type AttachmentRoutes = AttachmentsRoute | ReportAddAttachmentRoute; +type AttachmentRouteParams = AttachmentModalScreenParams; + +function getAttachmentModalScreenRoute(url: AttachmentRoutes, params?: AttachmentRouteParams) { + if (!params?.source) { + return url; + } + + const {source, attachmentID, type, reportID, accountID, isAuthTokenRequired, originalFileName, attachmentLink} = params; + + const sourceParam = `?source=${encodeURIComponent(source as string)}`; + const attachmentIDParam = attachmentID ? `&attachmentID=${attachmentID}` : ''; + const typeParam = type ? `&type=${type as string}` : ''; + const reportIDParam = reportID ? `&reportID=${reportID}` : ''; + const accountIDParam = accountID ? `&accountID=${accountID}` : ''; + const authTokenParam = isAuthTokenRequired ? '&isAuthTokenRequired=true' : ''; + const fileNameParam = originalFileName ? `&originalFileName=${originalFileName}` : ''; + const attachmentLinkParam = attachmentLink ? `&attachmentLink=${attachmentLink}` : ''; + + return `${url}${sourceParam}${typeParam}${reportIDParam}${attachmentIDParam}${accountIDParam}${authTokenParam}${fileNameParam}${attachmentLinkParam} ` as const; +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any type ExtractRouteName = TRoute extends {getRoute: (...args: any[]) => infer TRouteName} ? TRouteName : TRoute; diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx index 695674c00475b..5f499284d754e 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx @@ -10,7 +10,7 @@ import SafeAreaConsumer from '@components/SafeAreaConsumer'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import ReportAttachmentsContext from '@pages/home/report/ReportAttachmentsContext'; +import AttachmentModalContext from '@pages/media/AttachmentModalScreen/AttachmentModalContext'; import CONST from '@src/CONST'; type CarouselItemProps = { @@ -33,7 +33,7 @@ type CarouselItemProps = { function CarouselItem({item, onPress, isFocused, isModalHovered, reportID}: CarouselItemProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const {isAttachmentHidden} = useContext(ReportAttachmentsContext); + const {isAttachmentHidden} = useContext(AttachmentModalContext); const [isHidden, setIsHidden] = useState(() => (item.reportActionID && isAttachmentHidden(item.reportActionID)) ?? item.hasBeenFlagged); const renderButton = (style: StyleProp) => ( diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx index e650df1be6507..8fcb8ed050939 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx @@ -112,7 +112,16 @@ function ImageRenderer({tnode}: ImageRendererProps) { } const attachmentLink = tnode.parent?.attributes?.href; - const route = ROUTES.ATTACHMENTS?.getRoute(reportID, attachmentID, type, source, accountID, isAttachmentOrReceipt, fileName, attachmentLink); + const route = ROUTES.ATTACHMENTS?.getRoute({ + attachmentID, + reportID, + type, + source, + accountID, + isAuthTokenRequired: isAttachmentOrReceipt, + originalFileName: fileName, + attachmentLink, + }); Navigation.navigate(route); }} onLongPress={(event) => { diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx index 74525489adb7c..4efb88bc4f918 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx @@ -46,7 +46,7 @@ function VideoRenderer({tnode, key}: VideoRendererProps) { return; } const isAuthTokenRequired = !!htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]; - const route = ROUTES.ATTACHMENTS.getRoute(report?.reportID, attachmentID, type, sourceURL, accountID, isAuthTokenRequired, undefined, undefined, hashKey); + const route = ROUTES.ATTACHMENTS.getRoute({attachmentID, reportID: report?.reportID, type, source: sourceURL, accountID, isAuthTokenRequired, hashKey}); Navigation.navigate(route); }} /> diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 59aaeb3acf7ee..d40102010bc1b 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -62,6 +62,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type ReactComponentModule from '@src/types/utils/ReactComponentModule'; +import attachmentModalScreenOptions from './attachmentModalScreenOptions'; import createRootStackNavigator from './createRootStackNavigator'; import {screensWithEnteringAnimation, workspaceSplitsWithoutEnteringAnimation} from './createRootStackNavigator/GetStateForActionHandlers'; import defaultScreenOptions from './defaultScreenOptions'; @@ -88,7 +89,7 @@ type AuthScreensProps = { initialLastUpdateIDAppliedToClient: OnyxEntry; }; -const loadReportAttachments = () => require('../../../pages/home/report/ReportAttachments').default; +const loadAttachmentModalScreen = () => require('../../../pages/media/AttachmentModalScreen').default; const loadValidateLoginPage = () => require('../../../pages/ValidateLoginPage').default; const loadLogOutPreviousUserPage = () => require('../../../pages/LogOutPreviousUserPage').default; const loadConciergePage = () => require('../../../pages/ConciergePage').default; @@ -184,11 +185,11 @@ function handleNetworkReconnect() { } const RootStack = createRootStackNavigator(); + // We want to delay the re-rendering for components(e.g. ReportActionCompose) // that depends on modal visibility until Modal is completely closed and its focused // When modal screen is focused, update modal visibility in Onyx // https://reactnavigation.org/docs/navigation-events/ - const modalScreenListeners = { focus: () => { Modal.setModalVisibility(true, CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED); @@ -600,11 +601,8 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie /> require('@pages/home/ReportScreen').default; const loadSidebarScreen = () => require('@pages/home/sidebar/BaseSidebarScreen').default; - const Split = createSplitNavigator(); /** diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index f90f0f147a83c..9b3009642992d 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1761,6 +1761,7 @@ type ReportsSplitNavigatorParamList = { moneyRequestReportActionID?: string; transactionID?: string; }; + [SCREENS.ATTACHMENTS]: AttachmentModalScreenParams; }; type SettingsSplitNavigatorParamList = { @@ -1968,6 +1969,7 @@ type SharedScreensParamList = { shortLivedAuthToken?: string; shortLivedToken?: string; authTokenType?: ValueOf; + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents exitTo?: Routes | HybridAppRoute; shouldForceLogin: string; domain?: Routes; @@ -1976,6 +1978,7 @@ type SharedScreensParamList = { [SCREENS.VALIDATE_LOGIN]: { accountID: string; validateCode: string; + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents exitTo?: Routes | HybridAppRoute; }; }; diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 065cf7cc6bcd7..adec2049db412 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -152,6 +152,7 @@ import { import SelectionScraper from '@libs/SelectionScraper'; import shouldRenderAddPaymentCard from '@libs/shouldRenderAppPaymentCard'; import {ReactionListContext} from '@pages/home/ReportScreenContext'; +import AttachmentModalContext from '@pages/media/AttachmentModalScreen/AttachmentModalContext'; import variables from '@styles/variables'; import {openPersonalBankAccountSetupView} from '@userActions/BankAccounts'; import {hideEmojiPicker, isActive} from '@userActions/EmojiPickerAction'; @@ -181,7 +182,6 @@ import ReportActionItemMessage from './ReportActionItemMessage'; import ReportActionItemMessageEdit from './ReportActionItemMessageEdit'; import ReportActionItemSingle from './ReportActionItemSingle'; import ReportActionItemThread from './ReportActionItemThread'; -import ReportAttachmentsContext from './ReportAttachmentsContext'; import TripSummary from './TripSummary'; type PureReportActionItemProps = { @@ -442,7 +442,7 @@ function PureReportActionItem({ const [isHidden, setIsHidden] = useState(false); const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED); const reactionListRef = useContext(ReactionListContext); - const {updateHiddenAttachments} = useContext(ReportAttachmentsContext); + const {updateHiddenAttachments} = useContext(AttachmentModalContext); const composerTextInputRef = useRef(null); const popoverAnchorRef = useRef>(null); const downloadedPreviews = useRef([]); diff --git a/src/pages/home/report/ReportAttachments.tsx b/src/pages/home/report/ReportAttachments.tsx deleted file mode 100644 index 4896a06555b37..0000000000000 --- a/src/pages/home/report/ReportAttachments.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React, {useCallback, useEffect, useMemo} from 'react'; -import {useOnyx} from 'react-native-onyx'; -import AttachmentModal from '@components/AttachmentModal'; -import type {Attachment} from '@components/Attachments/types'; -import useNetwork from '@hooks/useNetwork'; -import {openReport} from '@libs/actions/Report'; -import ComposerFocusManager from '@libs/ComposerFocusManager'; -import Navigation from '@libs/Navigation/Navigation'; -import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {AuthScreensParamList} from '@libs/Navigation/types'; -import {isReportNotFound} from '@libs/ReportUtils'; -import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; - -type ReportAttachmentsProps = PlatformStackScreenProps; - -function ReportAttachments({route}: ReportAttachmentsProps) { - const reportID = route.params.reportID; - const attachmentID = route.params.attachmentID; - const type = route.params.type; - const hashKey = route.params.hashKey; - const accountID = route.params.accountID; - const isAuthTokenRequired = route.params.isAuthTokenRequired; - const attachmentLink = route.params.attachmentLink; - const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: true}); - const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { - canEvict: false, - canBeMissing: true, - }); - const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, { - canBeMissing: false, - }); - const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: false}); - const {isOffline} = useNetwork(); - const fileName = route.params?.originalFileName; - - // Extract the reportActionID from the attachmentID (format: reportActionID_index) - const reportActionID = useMemo(() => attachmentID?.split('_')?.[0], [attachmentID]); - - const shouldFetchReport = useMemo(() => { - return isEmptyObject(reportActions?.[reportActionID ?? CONST.DEFAULT_NUMBER_ID]); - }, [reportActions, reportActionID]); - - const isLoading = useMemo(() => { - if (isOffline || isReportNotFound(report) || !reportID) { - return false; - } - const isEmptyReport = isEmptyObject(report); - return !!isLoadingApp || isEmptyReport || (reportMetadata?.isLoadingInitialReportActions !== false && shouldFetchReport); - }, [isOffline, reportID, isLoadingApp, report, reportMetadata, shouldFetchReport]); - - // In native the imported images sources are of type number. Ref: https://reactnative.dev/docs/image#imagesource - const source = Number(route.params.source) || (typeof route.params.source === 'string' ? tryResolveUrlFromApiRoot(decodeURIComponent(route.params.source)) : undefined); - - const fetchReport = useCallback(() => { - openReport(reportID, reportActionID); - }, [reportID, reportActionID]); - - useEffect(() => { - if (!reportID || !shouldFetchReport) { - return; - } - - fetchReport(); - }, [reportID, fetchReport, shouldFetchReport]); - - const onCarouselAttachmentChange = useCallback( - (attachment: Attachment) => { - const routeToNavigate = ROUTES.ATTACHMENTS.getRoute( - reportID, - attachment.attachmentID, - type, - String(attachment.source), - Number(accountID), - attachment?.isAuthTokenRequired, - attachment?.file?.name, - attachment?.attachmentLink, - hashKey, - ); - Navigation.navigate(routeToNavigate); - }, - [reportID, type, accountID, hashKey], - ); - - return ( - { - Navigation.dismissModal(); - // This enables Composer refocus when the attachments modal is closed by the browser navigation - ComposerFocusManager.setReadyToFocus(); - }} - onCarouselAttachmentChange={onCarouselAttachmentChange} - shouldShowNotFoundPage={!isLoading && type !== CONST.ATTACHMENT_TYPE.SEARCH && !report?.reportID} - isAuthTokenRequired={!!isAuthTokenRequired} - attachmentLink={attachmentLink ?? ''} - originalFileName={fileName ?? ''} - /> - ); -} - -ReportAttachments.displayName = 'ReportAttachments'; - -export default ReportAttachments; diff --git a/src/pages/home/report/ReportAttachmentsContext.tsx b/src/pages/home/report/ReportAttachmentsContext.tsx deleted file mode 100644 index 0077cc27ad38a..0000000000000 --- a/src/pages/home/report/ReportAttachmentsContext.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, {useEffect, useMemo, useRef} from 'react'; -import useCurrentReportID from '@hooks/useCurrentReportID'; -import type ChildrenProps from '@src/types/utils/ChildrenProps'; - -type ReportAttachmentsContextValue = { - isAttachmentHidden: (reportActionID: string) => boolean; - updateHiddenAttachments: (reportActionID: string, isHidden: boolean) => void; -}; - -const ReportAttachmentsContext = React.createContext({ - isAttachmentHidden: () => false, - updateHiddenAttachments: () => {}, -}); - -function ReportAttachmentsProvider({children}: ChildrenProps) { - const currentReportID = useCurrentReportID(); - const hiddenAttachments = useRef>({}); - - useEffect(() => { - // We only want to store the attachment visibility for the current report. - // If the current report ID changes, clear the ref. - hiddenAttachments.current = {}; - }, [currentReportID?.currentReportID]); - - const contextValue = useMemo( - () => ({ - isAttachmentHidden: (reportActionID: string) => hiddenAttachments.current[reportActionID], - updateHiddenAttachments: (reportActionID: string, value: boolean) => { - hiddenAttachments.current = { - ...hiddenAttachments.current, - [reportActionID]: value, - }; - }, - }), - [], - ); - - return {children}; -} - -ReportAttachmentsProvider.displayName = 'ReportAttachmentsProvider'; - -export default ReportAttachmentsContext; -export {ReportAttachmentsProvider}; diff --git a/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent.tsx b/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent.tsx index 2a24a7a4dd24e..8a6cef2320361 100644 --- a/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent.tsx +++ b/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent.tsx @@ -39,7 +39,6 @@ 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 type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -50,7 +49,7 @@ type OnValidateFileCallback = (file: FileObject | undefined, setFile: (file: Fil type OnCloseOptions = { shouldCallDirectly?: boolean; - navigateBack?: () => void; + onAfterClose?: () => void; }; type AttachmentModalBaseContentProps = { @@ -132,9 +131,6 @@ type AttachmentModalBaseContentProps = { /** The link of the attachment */ attachmentLink?: string; - /** Fallback route when the modal is closed */ - fallbackRoute?: Route; - /** Determines if the attachment is invalid or not */ isAttachmentInvalid?: boolean; @@ -199,13 +195,12 @@ function AttachmentModalBaseContent({ shouldShowNotFoundPage = false, shouldDisableSendButton = false, shouldDisplayHelpButton = true, - fallbackRoute, isDeleteReceiptConfirmModalVisible = false, isAttachmentInvalid = false, attachmentInvalidReason, attachmentInvalidReasonTitle, submitRef, - onClose = () => Navigation.goBack(fallbackRoute), + onClose, onConfirm, onConfirmModalClose, onRequestDeleteReceipt, @@ -336,7 +331,7 @@ function AttachmentModalBaseContent({ const deleteAndCloseModal = useCallback(() => { detachReceipt(transaction?.transactionID); onDeleteReceipt?.(); - onClose(); + onClose?.(); }, [onClose, onDeleteReceipt, transaction?.transactionID]); // Close the modal when the escape key is pressed @@ -345,7 +340,7 @@ function AttachmentModalBaseContent({ const unsubscribeEscapeKey = KeyboardShortcut.subscribe( shortcutConfig.shortcutKey, () => { - onClose(); + onClose?.(); }, shortcutConfig.descriptionKey, shortcutConfig.modifiers, @@ -367,7 +362,7 @@ function AttachmentModalBaseContent({ icon: Expensicons.Camera, text: translate('common.replace'), onSelected: () => { - const navigateBack = () => { + const goToScanScreen = () => { InteractionManager.runAfterInteractions(() => { Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( @@ -381,7 +376,7 @@ function AttachmentModalBaseContent({ }); }; - onClose?.({shouldCallDirectly: true, navigateBack}); + onClose?.({shouldCallDirectly: true, onAfterClose: goToScanScreen}); }, }); } diff --git a/src/pages/media/AttachmentModalScreen/AttachmentModalContainer/index.native.tsx b/src/pages/media/AttachmentModalScreen/AttachmentModalContainer/index.native.tsx index a06ba3d88c813..795f7f84fdecb 100644 --- a/src/pages/media/AttachmentModalScreen/AttachmentModalContainer/index.native.tsx +++ b/src/pages/media/AttachmentModalScreen/AttachmentModalContainer/index.native.tsx @@ -1,5 +1,6 @@ import React, {memo, useCallback, useContext, useEffect} from 'react'; import ScreenWrapper from '@components/ScreenWrapper'; +import attachmentModalHandler from '@libs/AttachmentModalHandler'; import Navigation from '@libs/Navigation/Navigation'; import type {OnCloseOptions} from '@pages/media/AttachmentModalScreen/AttachmentModalBaseContent'; import AttachmentModalBaseContent from '@pages/media/AttachmentModalScreen/AttachmentModalBaseContent'; @@ -12,18 +13,21 @@ function AttachmentModalContainer({contentProps, navigation, onShow, onClose}: A const closeScreen = useCallback( (options?: OnCloseOptions) => { - onClose?.(); attachmentsContext.setCurrentAttachment(undefined); - // If a custom navigation callback is provided, call it instead of navigating back - if (options?.navigateBack) { - options?.navigateBack(); - return; - } + const close = () => { + onClose?.(); + Navigation.goBack(); + options?.onAfterClose?.(); + }; - Navigation.goBack(contentProps.fallbackRoute); + if (options?.shouldCallDirectly) { + close(); + } else { + attachmentModalHandler.handleModalClose(close); + } }, - [attachmentsContext, contentProps.fallbackRoute, onClose], + [attachmentsContext, onClose], ); useEffect(() => { diff --git a/src/pages/media/AttachmentModalScreen/AttachmentModalContainer/index.tsx b/src/pages/media/AttachmentModalScreen/AttachmentModalContainer/index.tsx index fe6db064fc4a1..300639114c3b3 100644 --- a/src/pages/media/AttachmentModalScreen/AttachmentModalContainer/index.tsx +++ b/src/pages/media/AttachmentModalScreen/AttachmentModalContainer/index.tsx @@ -1,7 +1,6 @@ import React, {useCallback, useContext, useEffect, useState} from 'react'; import {InteractionManager} from 'react-native'; import Modal from '@components/Modal'; -import attachmentModalHandler from '@libs/AttachmentModalHandler'; import Navigation from '@libs/Navigation/Navigation'; import type {OnCloseOptions} from '@pages/media/AttachmentModalScreen/AttachmentModalBaseContent'; import AttachmentModalBaseContent from '@pages/media/AttachmentModalScreen/AttachmentModalBaseContent'; @@ -9,9 +8,8 @@ import AttachmentModalContext from '@pages/media/AttachmentModalScreen/Attachmen import CONST from '@src/CONST'; import type AttachmentModalContainerProps from './types'; -const onCloseNoop = () => {}; - -function AttachmentModalContainer({contentProps, modalType, onShow, onClose}: AttachmentModalContainerProps) { +function AttachmentModalContainer({contentProps, modalType, onShow, onClose, shouldHandleNavigationBack}: AttachmentModalContainerProps) { + const [isVisible, setIsVisible] = useState(true); const attachmentsContext = useContext(AttachmentModalContext); const [shouldDisableAnimationAfterInitialMount, setShouldDisableAnimationAfterInitialMount] = useState(false); @@ -24,25 +22,17 @@ function AttachmentModalContainer({contentProps, modalType, onShow, onClose}: At */ const closeModal = useCallback( (options?: OnCloseOptions) => { - if (typeof onClose === 'function') { - if (options?.shouldCallDirectly) { - onClose(); - } else { - attachmentModalHandler.handleModalClose(onClose); - } - } - attachmentsContext.setCurrentAttachment(undefined); + setIsVisible(false); - // If a custom navigation callback is provided, call it instead of navigating back - if (options?.navigateBack) { - options?.navigateBack(); - return; - } + onClose?.(); + Navigation.dismissModal(); - Navigation.goBack(contentProps.fallbackRoute); + if (options?.onAfterClose) { + options?.onAfterClose(); + } }, - [attachmentsContext, contentProps.fallbackRoute, onClose], + [attachmentsContext, onClose], ); // After the modal has initially been mounted and animated in, @@ -61,9 +51,8 @@ function AttachmentModalContainer({contentProps, modalType, onShow, onClose}: At return ( { if (!contentProps.submitRef?.current) { @@ -71,6 +60,7 @@ function AttachmentModalContainer({contentProps, modalType, onShow, onClose}: At } return contentProps.submitRef.current; }} + shouldHandleNavigationBack={shouldHandleNavigationBack} > { - const routeToNavigate = ROUTES.ATTACHMENTS.getRoute( + const routeToNavigate = ROUTES.ATTACHMENTS.getRoute({ reportID, - attachment.attachmentID, + attachmentID: attachment.attachmentID, type, - String(attachment.source), + source: String(attachment.source), accountID, - attachment?.isAuthTokenRequired, - attachment?.file?.name, - attachment?.attachmentLink, + isAuthTokenRequired: attachment?.isAuthTokenRequired, + originalFileName: attachment?.file?.name, + attachmentLink: attachment?.attachmentLink, hashKey, - ); + }); Navigation.navigate(routeToNavigate); }, [reportID, type, accountID, hashKey], ); + const onClose = useCallback(() => { + // This enables Composer refocus when the attachments modal is closed by the browser navigation + ComposerFocusManager.setReadyToFocus(); + }, []); + /** * If our attachment is a PDF, return the unswipeable Modal type. */ @@ -230,6 +236,7 @@ function ReportAttachmentModalContent({route, navigation}: AttachmentModalScreen contentProps={contentProps} modalType={modalType} onShow={onShow} + onClose={onClose} /> ); } diff --git a/src/pages/media/AttachmentModalScreen/routes/ReportAvatarModalContent.tsx b/src/pages/media/AttachmentModalScreen/routes/ReportAvatarModalContent.tsx index c3bdd4236759c..94491fc5d246b 100644 --- a/src/pages/media/AttachmentModalScreen/routes/ReportAvatarModalContent.tsx +++ b/src/pages/media/AttachmentModalScreen/routes/ReportAvatarModalContent.tsx @@ -6,7 +6,6 @@ import type {AttachmentModalBaseContentProps} from '@pages/media/AttachmentModal import AttachmentModalContainer from '@pages/media/AttachmentModalScreen/AttachmentModalContainer'; import type {AttachmentModalScreenProps} from '@pages/media/AttachmentModalScreen/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; function ReportAvatarModalContent({navigation, route}: AttachmentModalScreenProps) { const {reportID, policyID} = route.params; @@ -37,11 +36,10 @@ function ReportAvatarModalContent({navigation, route}: AttachmentModalScreenProp () => ({ ...attachment, - fallbackRoute: ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID), shouldShowNotFoundPage: !report?.reportID && !isLoadingApp, isLoading: (!report?.reportID || !policy?.id) && !!isLoadingApp, }) satisfies Partial, - [attachment, isLoadingApp, policy?.id, report?.reportID, reportID], + [attachment, isLoadingApp, policy?.id, report?.reportID], ); return ( diff --git a/src/pages/media/AttachmentModalScreen/types.ts b/src/pages/media/AttachmentModalScreen/types.ts index d542fe283cf93..960595c1e69e6 100644 --- a/src/pages/media/AttachmentModalScreen/types.ts +++ b/src/pages/media/AttachmentModalScreen/types.ts @@ -24,6 +24,7 @@ type AttachmentModalModalProps = { modalType?: ModalType; onShow?: () => void; onClose?: () => void; + shouldHandleNavigationBack?: boolean; }; type AttachmentModalScreenParams = AttachmentModalBaseContentProps & diff --git a/tests/perf-test/ReportActionsList.perf-test.tsx b/tests/perf-test/ReportActionsList.perf-test.tsx index d3546621a6542..c9d38d50b54b1 100644 --- a/tests/perf-test/ReportActionsList.perf-test.tsx +++ b/tests/perf-test/ReportActionsList.perf-test.tsx @@ -4,12 +4,12 @@ import Onyx from 'react-native-onyx'; import {measureRenders} from 'reassure'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import type Navigation from '@libs/Navigation/Navigation'; +import {AttachmentModalContextProvider} from '@pages/media/AttachmentModalScreen/AttachmentModalContext'; import ComposeProviders from '@src/components/ComposeProviders'; import {LocaleContextProvider} from '@src/components/LocaleContextProvider'; import OnyxProvider from '@src/components/OnyxProvider'; import ONYXKEYS from '@src/ONYXKEYS'; import ReportActionsList from '@src/pages/home/report/ReportActionsList'; -import {ReportAttachmentsProvider} from '@src/pages/home/report/ReportAttachmentsContext'; import {ActionListContext, ReactionListContext} from '@src/pages/home/ReportScreenContext'; import type {PersonalDetailsList} from '@src/types/onyx'; import createRandomReportAction from '../utils/collections/reportActions'; @@ -106,7 +106,7 @@ afterEach(() => { function ReportActionsListWrapper() { const reportActions = ReportTestUtils.getMockedSortedReportActions(500); return ( - + { return render( - + diff --git a/tests/unit/CarouselItemTest.tsx b/tests/unit/CarouselItemTest.tsx index d25407d08b78d..420125b234fe4 100644 --- a/tests/unit/CarouselItemTest.tsx +++ b/tests/unit/CarouselItemTest.tsx @@ -6,7 +6,7 @@ import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxProvider from '@components/OnyxProvider'; import {PlaybackContextProvider} from '@components/VideoPlayerContexts/PlaybackContext'; import {translateLocal} from '@libs/Localize'; -import {ReportAttachmentsProvider} from '@pages/home/report/ReportAttachmentsContext'; +import {AttachmentModalContextProvider} from '@pages/media/AttachmentModalScreen/AttachmentModalContext'; import ONYXKEYS from '@src/ONYXKEYS'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -20,7 +20,7 @@ describe('CarouselItem', () => { - + { }} isFocused /> - + ,