diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 16c126be5dfe8..8bac46cdf4065 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -91,6 +91,17 @@ const ROUTES = { return getUrlWithBackToParam(baseRoute, backTo); }, }, + + EXPENSE_REPORT_RHP: { + route: 'e/:reportID', + getRoute: ({reportID, backTo}: {reportID: string; backTo?: string}) => { + const baseRoute = `e/${reportID}` as const; + + // eslint-disable-next-line no-restricted-syntax -- Legacy route generation + return getUrlWithBackToParam(baseRoute, backTo); + }, + }, + SEARCH_REPORT_VERIFY_ACCOUNT: { route: `search/view/:reportID/${VERIFY_ACCOUNT}`, getRoute: (reportID: string) => `search/view/${reportID}/${VERIFY_ACCOUNT}` as const, @@ -593,6 +604,10 @@ const ROUTES = { route: `r/:reportID/${VERIFY_ACCOUNT}`, getRoute: (reportID: string) => `r/${reportID}/${VERIFY_ACCOUNT}` as const, }, + EXPENSE_REPORT_VERIFY_ACCOUNT: { + route: `e/:reportID/${VERIFY_ACCOUNT}`, + getRoute: (reportID: string) => `e/${reportID}/${VERIFY_ACCOUNT}` as const, + }, REPORT_PARTICIPANTS: { route: 'r/:reportID/participants', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 989a9ae3fe92d..7ff78424f6a3f 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -255,8 +255,6 @@ const SCREENS = { TRAVEL: 'Travel', SEARCH_REPORT: 'SearchReport', SEARCH_REPORT_ACTIONS: 'SearchReportActions', - // These two routes will be added in a separate PR adding Super Wide RHP routes - EXPENSE_REPORT: 'ExpenseReport', SEARCH_MONEY_REQUEST_REPORT: 'SearchMoneyRequestReport', SEARCH_COLUMNS: 'SearchColumns', @@ -277,6 +275,7 @@ const SCREENS = { MERGE_TRANSACTION: 'MergeTransaction', REPORT_CARD_ACTIVATE: 'Report_Card_Activate', DOMAIN: 'Domain', + EXPENSE_REPORT: 'ExpenseReport', }, REPORT_CARD_ACTIVATE: 'Report_Card_Activate_Root', PUBLIC_CONSOLE_DEBUG: 'Console_Debug', @@ -826,6 +825,7 @@ const SCREENS = { REIMBURSEMENT_ACCOUNT_ENTER_SIGNER_INFO: 'Reimbursement_Account_Signer_Info', REFERRAL_DETAILS: 'Referral_Details', REPORT_VERIFY_ACCOUNT: 'Report_Verify_Account', + EXPENSE_REPORT_VERIFY_ACCOUNT: 'Expense_Report_Verify_Account', KEYBOARD_SHORTCUTS: 'KeyboardShortcuts', SHARE: { ROOT: 'Share_Root', diff --git a/src/components/AddUnreportedExpenseFooter.tsx b/src/components/AddUnreportedExpenseFooter.tsx index 58defd3616309..b7b2355a93a11 100644 --- a/src/components/AddUnreportedExpenseFooter.tsx +++ b/src/components/AddUnreportedExpenseFooter.tsx @@ -51,7 +51,7 @@ function AddUnreportedExpenseFooter({selectedIds, report, reportToConfirm, repor setErrorMessage(translate('iou.selectUnreportedExpense')); return; } - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { if (report && isIOUReport(report)) { diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 11a7328031ae0..5e3c584bbafd7 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -24,6 +24,7 @@ import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals'; import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsActions'; import useStrictPolicyRules from '@hooks/useStrictPolicyRules'; @@ -45,7 +46,7 @@ import Log from '@libs/Log'; import {getThreadReportIDsForTransactions, getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList, SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; +import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@libs/Navigation/types'; import { buildOptimisticNextStepForDEWOfflineSubmission, buildOptimisticNextStepForDynamicExternalWorkflowError, @@ -156,7 +157,6 @@ import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu'; import {useSearchContext} from './Search/SearchContext'; import AnimatedSettlementButton from './SettlementButton/AnimatedSettlementButton'; import Text from './Text'; -import {WideRHPContext} from './WideRHPContextProvider'; type MoneyReportHeaderProps = { /** The report currently being looked at */ @@ -197,7 +197,8 @@ function MoneyReportHeader({ const shouldDisplayNarrowVersion = shouldUseNarrowLayout || isMediumScreenWidth; const route = useRoute< | PlatformStackRouteProp - | PlatformStackRouteProp + | PlatformStackRouteProp + | PlatformStackRouteProp | PlatformStackRouteProp >(); const {login: currentUserLogin, accountID, email} = useCurrentUserPersonalDetails(); @@ -402,9 +403,11 @@ function MoneyReportHeader({ const shouldCalculateTotals = useSearchShouldCalculateTotals(currentSearchKey, currentSearchQueryJSON?.similarSearchHash, true); const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON?.hash}`, {canBeMissing: true}); - const {wideRHPRouteKeys} = useContext(WideRHPContext); const [network] = useOnyx(ONYXKEYS.NETWORK, {canBeMissing: true}); - const shouldDisplayNarrowMoreButton = !shouldDisplayNarrowVersion || (wideRHPRouteKeys.length > 0 && !isSmallScreenWidth); + + const {isWideRHPDisplayedOnWideLayout, isSuperWideRHPDisplayedOnWideLayout} = useResponsiveLayoutOnWideRHP(); + + const shouldDisplayNarrowMoreButton = !shouldDisplayNarrowVersion || isWideRHPDisplayedOnWideLayout || isSuperWideRHPDisplayedOnWideLayout; const showExportProgressModal = useCallback(() => { return showConfirmModal({ @@ -509,9 +512,9 @@ function MoneyReportHeader({ const shouldShowLoadingBar = useLoadingBarVisibility(); const kycWallRef = useContext(KYCWallContext); - const isReportInRHP = route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT; + const isReportInRHP = route.name !== SCREENS.REPORT; const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth; - const isReportInSearch = route.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT; + const isReportInSearch = route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT || route.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT; const isReportSubmitter = isCurrentUserSubmitter(chatIOUReport); const isChatReportDM = isDM(chatReport); @@ -1549,7 +1552,9 @@ function MoneyReportHeader({ const backToRoute = route.params?.backTo ?? (chatReport?.reportID ? ROUTES.REPORT_WITH_ID.getRoute(chatReport.reportID) : undefined); Navigation.goBack(backToRoute); } - handleDeleteTransactions(); + // It has been handled like the rest of the delete cases. It will be refactored along with other cases. + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => handleDeleteTransactions()); }); }, [showConfirmModal, translate, selectedTransactionIDs.length, transactions, handleDeleteTransactions, route.params?.backTo, chatReport?.reportID]); @@ -1731,7 +1736,7 @@ function MoneyReportHeader({ {isReportInSearch && ( )} diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 50808f30e565c..73a130e6637ef 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -99,7 +99,7 @@ type MoneyRequestHeaderProps = { function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPress}: MoneyRequestHeaderProps) { // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use a correct layout for the hold expense modal https://github.com/Expensify/App/pull/47990#issuecomment-2362382026 // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); + const {shouldUseNarrowLayout, isSmallScreenWidth, isInNarrowPaneModal} = useResponsiveLayout(); const route = useRoute< PlatformStackRouteProp | PlatformStackRouteProp >(); @@ -570,6 +570,11 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre deleteTransactions([transaction.transactionID], duplicateTransactions, duplicateTransactionViolations, currentSearchHash, true); removeTransaction(transaction.transactionID); } + if (isInNarrowPaneModal) { + Navigation.navigateBackToLastSuperWideRHPScreen(); + return; + } + onBackButtonPress(); }} onCancel={() => setIsDeleteModalVisible(false)} diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index 7de13fef2bfb7..df2e7d475a595 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -30,7 +30,7 @@ import useParentReportAction from '@hooks/useParentReportAction'; import usePrevious from '@hooks/usePrevious'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useReportScrollManager from '@hooks/useReportScrollManager'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsActions'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -172,7 +172,7 @@ function MoneyRequestReportActionsList({ const isReportArchived = useReportIsArchived(reportID); const canPerformWriteAction = canUserPerformWriteAction(report, isReportArchived); - const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP(); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${getNonEmptyStringOnyxID(reportID)}`, {canBeMissing: true}); diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportGroupHeader.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportGroupHeader.tsx index cd9ba8646637c..5d561637bcfe4 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportGroupHeader.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportGroupHeader.tsx @@ -4,7 +4,7 @@ import Checkbox from '@components/Checkbox'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; import useThemeStyles from '@hooks/useThemeStyles'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import {getCommaSeparatedTagNameWithSanitizedColons} from '@libs/PolicyUtils'; @@ -64,7 +64,7 @@ function MoneyRequestReportGroupHeader({ }: MoneyRequestReportGroupHeaderProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP(); const cleanedGroupName = isGroupedByTag && group.groupName ? getCommaSeparatedTagNameWithSanitizedColons(group.groupName) : group.groupName; const displayName = cleanedGroupName || translate(isGroupedByTag ? 'reportLayout.noTag' : 'reportLayout.uncategorized'); diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx index badcc79500450..349db531a5816 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx @@ -8,6 +8,7 @@ import TransactionItemRow from '@components/TransactionItemRow'; import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; @@ -81,7 +82,8 @@ function MoneyRequestReportTransactionItem({ const {translate} = useLocalize(); const styles = useThemeStyles(); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {isSmallScreenWidth, isMediumScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); + const {isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout(); + const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP(); const theme = useTheme(); const isPendingDelete = isTransactionPendingDelete(transaction); const pendingAction = getTransactionPendingAction(transaction); diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index 5f8d6ac0eaa99..4a9598349e7cb 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -21,6 +21,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useOnyx from '@hooks/useOnyx'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; @@ -168,7 +169,8 @@ function MoneyRequestReportTransactionList({ const expensifyIcons = useMemoizedLazyExpensifyIcons(['Location', 'CheckSquare', 'ReceiptPlus']); const {translate, localeCompare} = useLocalize(); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {shouldUseNarrowLayout, isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout(); + const {isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout(); + const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP(); const {markReportIDAsExpense} = useContext(WideRHPContext); const [isModalVisible, setIsModalVisible] = useState(false); const [selectedTransactionID, setSelectedTransactionID] = useState(''); diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx index 1d821678276d8..f96ce35ec9f38 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx @@ -1,10 +1,14 @@ import {PortalHost} from '@gorhom/portal'; import React, {useCallback, useEffect, useMemo} from 'react'; -import {InteractionManager, View} from 'react-native'; +// We use Animated for all functionality related to wide RHP to make it easier +// to interact with react-navigation components (e.g., CardContainer, interpolator), which also use Animated. +// eslint-disable-next-line no-restricted-imports +import {Animated, InteractionManager, ScrollView, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import MoneyReportHeader from '@components/MoneyReportHeader'; import MoneyRequestHeader from '@components/MoneyRequestHeader'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import MoneyRequestReceiptView from '@components/ReportActionItem/MoneyRequestReceiptView'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import ReportHeaderSkeletonView from '@components/ReportHeaderSkeletonView'; import useNetwork from '@hooks/useNetwork'; @@ -12,6 +16,7 @@ import useNewTransactions from '@hooks/useNewTransactions'; import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import useParentReportAction from '@hooks/useParentReportAction'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; import {removeFailedReport} from '@libs/actions/Report'; @@ -56,6 +61,16 @@ function goBackFromSearchMoneyRequest() { const rootState = navigationRef.getRootState(); const lastRoute = rootState.routes.at(-1); + if (!lastRoute) { + Log.hmmm('[goBackFromSearchMoneyRequest()] No last route found in root state.'); + return; + } + + if (lastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { + Navigation.goBack(); + return; + } + if (lastRoute?.name !== NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR) { Log.hmmm('[goBackFromSearchMoneyRequest()] goBackFromSearchMoneyRequest was called from a different navigator than SearchFullscreenNavigator.'); return; @@ -84,6 +99,10 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe const styles = useThemeStyles(); const {isOffline} = useNetwork(); + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {isSmallScreenWidth} = useResponsiveLayout(); + + const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: false}); const reportID = report?.reportID; const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); const [isComposerFullSize = false] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`, {canBeMissing: true}); @@ -124,11 +143,14 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe // Prevent the empty state flash by ensuring transaction data is fully loaded before deciding which view to render // We need to wait for both the selector to finish AND ensure we're not in a loading state where transactions could still populate - const shouldWaitForTransactions = shouldWaitForTransactionsUtil(report, transactions, reportMetadata); + const shouldWaitForTransactions = shouldWaitForTransactionsUtil(report, transactions, reportMetadata, isOffline); const isEmptyTransactionReport = visibleTransactions && visibleTransactions.length === 0 && transactionThreadReportID === undefined; const shouldDisplayMoneyRequestActionsList = !!isEmptyTransactionReport || shouldDisplayReportTableView(report, visibleTransactions ?? []); + const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, {canBeMissing: true}); + const shouldShowWideRHPReceipt = visibleTransactions.length === 1 && !isSmallScreenWidth && !!transactionThreadReport; + const reportHeaderView = useMemo( () => isTransactionThreadView ? ( @@ -223,48 +245,62 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe contentContainerStyle={styles.flex1} errorRowStyles={[styles.ph5, styles.mv2]} > - - {shouldDisplayMoneyRequestActionsList ? ( - - ) : ( - + + {shouldShowWideRHPReceipt && ( + + + + + )} - {shouldDisplayReportFooter ? ( - <> - + {shouldDisplayMoneyRequestActionsList ? ( + + ) : ( + - - - ) : null} + )} + {shouldDisplayReportFooter ? ( + <> + + + + ) : null} + diff --git a/src/components/Navigation/SearchSidebar.tsx b/src/components/Navigation/SearchSidebar.tsx index dffea363935b2..bf9bd4d44505d 100644 --- a/src/components/Navigation/SearchSidebar.tsx +++ b/src/components/Navigation/SearchSidebar.tsx @@ -55,7 +55,7 @@ function SearchSidebar({state}: SearchSidebarProps) { setLastSearchType(currentSearchResults.type); }, [lastSearchType, queryJSON, setLastSearchType, currentSearchResults?.type]); - const shouldShowLoadingState = route?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT ? false : !isOffline && !!currentSearchResults?.isLoading; + const shouldShowLoadingState = route?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT ? false : !isOffline && !!currentSearchResults?.isLoading; if (shouldUseNarrowLayout) { return null; diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 301b09e06ceba..e7fde9e2b44c5 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -12,7 +12,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; import Navigation from '@libs/Navigation/Navigation'; -import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; +import type {RightModalNavigatorParamList} from '@libs/Navigation/types'; import {getReportAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction as canUserPerformWriteActionReportUtils, isMoneyRequestReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; @@ -91,8 +91,23 @@ function ParentNavigationSubtitle({ const isReportArchived = useReportIsArchived(report?.reportID); const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(report, isReportArchived); const isReportInRHP = currentRoute.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT; - const currentFullScreenRoute = useRootNavigationState((state) => state?.routes?.findLast((route) => isFullScreenName(route.name))); const hasAccessToParentReport = currentReport?.hasParentAccess !== false; + const {currentFullScreenRoute, currentFocusedNavigator} = useRootNavigationState((state) => { + const fullScreenRoute = state?.routes?.findLast((route) => isFullScreenName(route.name)); + + // We need to track which navigator is focused to handle parent report navigation correctly: + // if we are in RHP, and parent report is opened in RHP, we want to go back to the parent report + const focusedNavigator = state?.routes + ? state.routes.findLast((route) => { + return route.state?.routes && route.state.routes.length > 0; + }) + : undefined; + + return { + currentFullScreenRoute: fullScreenRoute, + currentFocusedNavigator: focusedNavigator, + }; + }); // We should not display the parent navigation subtitle if the user does not have access to the parent chat (the reportName is empty in this case) if (!reportName) { @@ -103,16 +118,22 @@ function ParentNavigationSubtitle({ const parentAction = getReportAction(parentReportID, parentReportActionID); const isVisibleAction = shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? CONST.DEFAULT_NUMBER_ID, canUserPerformWriteAction); + const focusedNavigatorState = currentFocusedNavigator?.state; + const currentReportIndex = focusedNavigatorState?.index; + if (openParentReportInCurrentTab && isReportInRHP) { // If the report is displayed in RHP in Reports tab, we want to stay in the current tab after opening the parent report if (currentFullScreenRoute?.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR && isMoneyRequestReport(report)) { - const lastRoute = currentFullScreenRoute?.state?.routes.at(-1); - if (lastRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT) { - const moneyRequestReportID = (lastRoute?.params as SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.MONEY_REQUEST_REPORT])?.reportID; - // If the parent report is already displayed underneath RHP, simply dismiss the modal - if (moneyRequestReportID === parentReportID) { - Navigation.dismissModal(); - return; + // Dismiss wide RHP and go back to already opened super wide RHP if the parent report is already opened there + if (currentFocusedNavigator?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && currentReportIndex && currentReportIndex > 0) { + const previousRoute = focusedNavigatorState.routes[currentReportIndex - 1]; + + if (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT) { + const moneyRequestReportID = (previousRoute?.params as RightModalNavigatorParamList[typeof SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT])?.reportID; + if (moneyRequestReportID === parentReportID) { + Navigation.goBack(); + return; + } } } @@ -129,12 +150,27 @@ function ParentNavigationSubtitle({ // When viewing a money request in the search navigator, open the parent report in a right-hand pane (RHP) // to preserve the search context instead of navigating away. - if (openParentReportInCurrentTab && currentFullScreenRoute?.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR) { - const lastRoute = currentFullScreenRoute?.state?.routes.at(-1); - if (lastRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT) { + if (openParentReportInCurrentTab && currentFocusedNavigator?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { + const lastRoute = currentFocusedNavigator?.state?.routes.at(-1); + if (lastRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT) { Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: parentReportID, reportActionID: parentReportActionID})); return; } + + // Specific case: when opening expense report from search report (chat RHP), + // avoid stacking RHPs by going back to the search report if it's already there + const previousRoute = currentFocusedNavigator?.state?.routes.at(-2); + + if (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT && lastRoute?.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT) { + if (previousRoute.params && 'reportID' in previousRoute.params) { + const reportIDFromParams = previousRoute.params.reportID; + + if (reportIDFromParams === parentReportID) { + Navigation.goBack(); + return; + } + } + } } if (isVisibleAction) { diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index f8d4347c71ac9..bb8c47f07a559 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -159,7 +159,8 @@ function MoneyRequestReceiptView({ const transactionToCheck = updatedTransaction ?? transaction; const doesTransactionHaveReceipt = !!transactionToCheck?.receipt && !isEmptyObject(transactionToCheck?.receipt); - const shouldShowReceiptEmptyState = !isInvoice && !hasReceipt && !!transactionToCheck && !doesTransactionHaveReceipt; + // Empty state for invoices should be displayed only in WideRHP + const shouldShowReceiptEmptyState = (isDisplayedInWideRHP || !isInvoice) && !hasReceipt && !!transactionToCheck && !doesTransactionHaveReceipt; const [receiptImageViolations, receiptViolations] = useMemo(() => { const imageViolations = []; diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index c2819921fd91c..504981d3488f2 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -143,7 +143,8 @@ function MoneyRequestReportPreviewContent({ const StyleUtils = useStyleUtils(); const {translate, formatPhoneNumber} = useLocalize(); const {isOffline} = useNetwork(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const currentUserDetails = useCurrentUserPersonalDetails(); const currentUserAccountID = currentUserDetails.accountID; const currentUserEmail = currentUserDetails.email ?? ''; @@ -517,9 +518,14 @@ function MoneyRequestReportPreviewContent({ name: 'MoneyRequestReportPreviewContent', op: CONST.TELEMETRY.SPAN_OPEN_REPORT, }); - - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, Navigation.getActiveRoute())); - }, [iouReportID]); + // Small screens navigate to full report view since super wide RHP + // is not available on narrow layouts and would break the navigation logic. + if (isSmallScreenWidth) { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, Navigation.getActiveRoute())); + } else { + Navigation.navigate(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: iouReportID, backTo: Navigation.getActiveRoute()})); + } + }, [iouReportID, isSmallScreenWidth]); const isDEWPolicy = hasDynamicExternalWorkflow(policy); const isDEWSubmitPending = hasPendingDEWSubmit(iouReportMetadata, isDEWPolicy); diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx index 3de2093e0fb32..0ddd856501be4 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx @@ -41,7 +41,8 @@ function MoneyRequestReportPreview({ }: MoneyRequestReportPreviewProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const personalDetailsList = usePersonalDetails(); const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`]; const invoiceReceiverPolicy = @@ -110,8 +111,14 @@ function MoneyRequestReportPreview({ name: 'MoneyRequestReportPreview', op: CONST.TELEMETRY.SPAN_OPEN_REPORT, }); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, Navigation.getActiveRoute())); - }, [iouReportID]); + // Small screens navigate to full report view since super wide RHP + // is not available on narrow layouts and would break the navigation logic. + if (isSmallScreenWidth) { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, Navigation.getActiveRoute())); + } else { + Navigation.navigate(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: iouReportID, backTo: Navigation.getActiveRoute()})); + } + }, [iouReportID, isSmallScreenWidth]); const renderItem: ListRenderItem = ({item}) => ( 1) { + markReportIDAsMultiTransactionExpense(reportID); + } else { + unmarkReportIDAsMultiTransactionExpense(reportID); + } + requestAnimationFrame(() => Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID, backTo}))); return; } @@ -840,7 +848,16 @@ function Search({ requestAnimationFrame(() => Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID, backTo}))); }, - [isMobileSelectionModeEnabled, toggleTransaction, queryJSON, handleSearch, searchKey, markReportIDAsExpense], + [ + isMobileSelectionModeEnabled, + markReportIDAsExpense, + toggleTransaction, + queryJSON, + handleSearch, + searchKey, + markReportIDAsMultiTransactionExpense, + unmarkReportIDAsMultiTransactionExpense, + ], ); const currentColumns = useMemo(() => { diff --git a/src/components/SelectionList/ListItem/SplitListItem.tsx b/src/components/SelectionList/ListItem/SplitListItem.tsx index fbf5215d7869d..548c4694b7cd6 100644 --- a/src/components/SelectionList/ListItem/SplitListItem.tsx +++ b/src/components/SelectionList/ListItem/SplitListItem.tsx @@ -1,5 +1,5 @@ -import React, {useCallback, useLayoutEffect, useRef, useState} from 'react'; -import {View} from 'react-native'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import {InteractionManager, View} from 'react-native'; import Icon from '@components/Icon'; import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; @@ -63,13 +63,19 @@ function SplitListItem({ }, [onInputFocus, item]); // Auto-focus input when item is selected and screen transition ends - useLayoutEffect(() => { - if (!splitItem.isSelected || !splitItem.isEditable || !didScreenTransitionEnd || !inputRef.current) { + useEffect(() => { + if (!didScreenTransitionEnd || !splitItem.isSelected || !splitItem.isEditable || !inputRef.current) { return; } - inputRef.current.focus(); - }, [splitItem.isSelected, splitItem.isEditable, didScreenTransitionEnd]); + // Use InteractionManager to ensure input focus happens after all animations/interactions complete. + // This prevents focus from interrupting modal close/open animations which would cause UI glitches + // and "jumping" behavior when quickly navigating between screens. + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + inputRef.current?.focus(); + }); + }, [didScreenTransitionEnd, splitItem.isSelected, splitItem.isEditable]); const inputCallbackRef = (ref: BaseTextInputRef | null) => { inputRef.current = ref; diff --git a/src/components/SidePanel/SidePanelModal/index.tsx b/src/components/SidePanel/SidePanelModal/index.tsx index 467da113af98e..60fb40444e7ce 100644 --- a/src/components/SidePanel/SidePanelModal/index.tsx +++ b/src/components/SidePanel/SidePanelModal/index.tsx @@ -27,8 +27,9 @@ function SidePanelModal({children, sidePanelTranslateX, closeSidePanel, shouldHi const [isRHPVisible = false] = useOnyx(ONYXKEYS.MODAL, {selector: isRHPVisibleSelector, canBeMissing: true}); const uniqueModalId = ComposerFocusManager.getId(); - const {wideRHPRouteKeys, isWideRHPFocused} = useContext(WideRHPContext); - const isWideRHPVisible = !!wideRHPRouteKeys.length; + const {wideRHPRouteKeys, isWideRHPFocused, superWideRHPRouteKeys, isSuperWideRHPFocused} = useContext(WideRHPContext); + + const shouldOverlayBeVisible = (!!wideRHPRouteKeys.length && isWideRHPFocused) || (!!superWideRHPRouteKeys.length && isSuperWideRHPFocused) || !isRHPVisible; const onCloseSidePanelOnSmallScreens = () => { if (isExtraLargeScreenWidth) { @@ -65,7 +66,7 @@ function SidePanelModal({children, sidePanelTranslateX, closeSidePanel, shouldHi {!shouldHideSidePanelBackdrop && ( )} diff --git a/src/components/TransactionItemRow/ReceiptPreview/index.tsx b/src/components/TransactionItemRow/ReceiptPreview/index.tsx index be3c27af98846..9a7da1e77ebaa 100644 --- a/src/components/TransactionItemRow/ReceiptPreview/index.tsx +++ b/src/components/TransactionItemRow/ReceiptPreview/index.tsx @@ -8,7 +8,7 @@ import DistanceEReceipt from '@components/DistanceEReceipt'; import EReceiptWithSizeCalculation from '@components/EReceiptWithSizeCalculation'; import type {ImageOnLoadEvent} from '@components/Image/types'; import useDebouncedState from '@hooks/useDebouncedState'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {hasReceiptSource, isDistanceRequest, isManualDistanceRequest, isPerDiemRequest} from '@libs/TransactionUtils'; @@ -40,7 +40,7 @@ function ReceiptPreview({source, hovered, isEReceipt = false, transactionItem}: const [imageAspectRatio, setImageAspectRatio] = useState(undefined); const [distanceEReceiptAspectRatio, setDistanceEReceiptAspectRatio] = useState(undefined); const [shouldShow, debounceShouldShow, setShouldShow] = useDebouncedState(false, CONST.TIMING.SHOW_HOVER_PREVIEW_DELAY); - const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP(); const hasMeasured = useRef(false); const {windowHeight} = useWindowDimensions(); const [isLoading, setIsLoading] = useState(true); diff --git a/src/components/VideoPlayerContexts/PlaybackContext/playbackContextReportIDUtils.ts b/src/components/VideoPlayerContexts/PlaybackContext/playbackContextReportIDUtils.ts index aec9590bd6408..f406e03c62180 100644 --- a/src/components/VideoPlayerContexts/PlaybackContext/playbackContextReportIDUtils.ts +++ b/src/components/VideoPlayerContexts/PlaybackContext/playbackContextReportIDUtils.ts @@ -47,7 +47,13 @@ const getCurrentRouteReportID: (url: string) => string | ProtectedCurrentRouteRe return isFocusedRouteAChatThread ? firstReportThatHasURLInAttachments : focusedRouteReportID; }; -const screensWithReportID = [SCREENS.RIGHT_MODAL.SEARCH_REPORT, SCREENS.REPORT, SCREENS.SEARCH.MONEY_REQUEST_REPORT, SCREENS.REPORT_ATTACHMENTS]; +const screensWithReportID = [ + SCREENS.RIGHT_MODAL.SEARCH_REPORT, + SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, + SCREENS.RIGHT_MODAL.EXPENSE_REPORT, + SCREENS.REPORT, + SCREENS.REPORT_ATTACHMENTS, +]; function hasReportIdInRouteParams(route: SearchRoute): route is RouteWithReportIDInParams { return !!route && !!route.params && !!screensWithReportID.find((screen) => screen === route.name) && 'reportID' in route.params; diff --git a/src/components/WideRHPContextProvider/default.ts b/src/components/WideRHPContextProvider/default.ts index 7add883bb4a18..10a359bfcb9cc 100644 --- a/src/components/WideRHPContextProvider/default.ts +++ b/src/components/WideRHPContextProvider/default.ts @@ -20,6 +20,8 @@ const defaultWideRHPContextValue: WideRHPContextType = { removeSuperWideRHPRouteKey: () => {}, syncRHPKeys: () => {}, clearWideRHPKeys: () => {}, + setIsWideRHPClosing: () => {}, + setIsSuperWideRHPClosing: () => {}, }; export default defaultWideRHPContextValue; diff --git a/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts b/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts index 6c945a7fa5a35..4d1ff9b4c210f 100644 --- a/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts +++ b/src/components/WideRHPContextProvider/getVisibleRHPRouteKeys.ts @@ -23,7 +23,6 @@ function getVisibleRHPKeys(allSuperWideRHPKeys: string[], allWideRHPKeys: string } const rootState = navigationRef.getRootState(); - if (!rootState) { return emptyRHPKeysState; } diff --git a/src/components/WideRHPContextProvider/index.tsx b/src/components/WideRHPContextProvider/index.tsx index 941c3d20b6dff..c697c1a846b40 100644 --- a/src/components/WideRHPContextProvider/index.tsx +++ b/src/components/WideRHPContextProvider/index.tsx @@ -1,5 +1,5 @@ import {findFocusedRoute} from '@react-navigation/native'; -import React, {createContext, useCallback, useEffect, useMemo, useState} from 'react'; +import React, {createContext, useCallback, useEffect, useMemo, useRef, useState} from 'react'; // We use Animated for all functionality related to wide RHP to make it easier // to interact with react-navigation components (e.g., CardContainer, interpolator), which also use Animated. // eslint-disable-next-line no-restricted-imports @@ -111,6 +111,17 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) { const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: expenseReportSelector, canBeMissing: true}); + const isWideRHPClosingRef = useRef(false); + const isSuperWideRHPClosingRef = useRef(false); + + const setIsWideRHPClosing = useCallback((isClosing: boolean) => { + isWideRHPClosingRef.current = isClosing; + }, []); + + const setIsSuperWideRHPClosing = useCallback((isClosing: boolean) => { + isSuperWideRHPClosingRef.current = isClosing; + }, []); + const {focusedRoute, focusedNavigator} = useRootNavigationState((state) => { if (!state) { return {focusedRoute: undefined, focusedNavigator: undefined}; @@ -333,6 +344,8 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) { isSuperWideRHPFocused, syncRHPKeys, clearWideRHPKeys, + setIsWideRHPClosing, + setIsSuperWideRHPClosing, }), [ wideRHPRouteKeys, @@ -354,6 +367,8 @@ function WideRHPContextProvider({children}: React.PropsWithChildren) { isSuperWideRHPFocused, syncRHPKeys, clearWideRHPKeys, + setIsWideRHPClosing, + setIsSuperWideRHPClosing, ], ); diff --git a/src/components/WideRHPContextProvider/types.ts b/src/components/WideRHPContextProvider/types.ts index 8ffb8e7dca87e..20300f48287e8 100644 --- a/src/components/WideRHPContextProvider/types.ts +++ b/src/components/WideRHPContextProvider/types.ts @@ -57,6 +57,12 @@ type WideRHPContextType = { // Clear the arrays of wide and super wide rhp keys clearWideRHPKeys: () => void; + + // Set that wide rhp is closing + setIsWideRHPClosing: (isClosing: boolean) => void; + + // Set that super wide rhp is closing + setIsSuperWideRHPClosing: (isClosing: boolean) => void; }; // eslint-disable-next-line import/prefer-default-export diff --git a/src/hooks/useResponsiveLayoutOnWideRHP/index.native.ts b/src/hooks/useResponsiveLayoutOnWideRHP/index.native.ts new file mode 100644 index 0000000000000..505b966964c19 --- /dev/null +++ b/src/hooks/useResponsiveLayoutOnWideRHP/index.native.ts @@ -0,0 +1,13 @@ +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import type ResponsiveLayoutOnWideRHPResult from './types'; + +// Super Wide and Wide RHPs are not displayed on native platforms. +export default function useResponsiveLayoutOnWideRHP(): ResponsiveLayoutOnWideRHPResult { + const responsiveLayoutValues = useResponsiveLayout(); + + return { + ...responsiveLayoutValues, + isWideRHPDisplayedOnWideLayout: false, + isSuperWideRHPDisplayedOnWideLayout: false, + }; +} diff --git a/src/hooks/useResponsiveLayoutOnWideRHP/index.ts b/src/hooks/useResponsiveLayoutOnWideRHP/index.ts new file mode 100644 index 0000000000000..8919d15bdfe60 --- /dev/null +++ b/src/hooks/useResponsiveLayoutOnWideRHP/index.ts @@ -0,0 +1,33 @@ +import {useRoute} from '@react-navigation/native'; +import {useContext} from 'react'; +import {WideRHPContext} from '@components/WideRHPContextProvider'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import type ResponsiveLayoutOnWideRHPResult from './types'; + +/** + * useResponsiveLayoutOnWideRHP is a wrapper on useResponsiveLayout. shouldUseNarrowLayout on a wide screen is true when the screen is displayed in RHP. + * In this hook this value is modified when the screen is displayed in Wide/Super Wide RHP, then in wide screen this value is false. + */ +export default function useResponsiveLayoutOnWideRHP(): ResponsiveLayoutOnWideRHPResult { + const route = useRoute(); + + const responsiveLayoutValues = useResponsiveLayout(); + + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {isSmallScreenWidth, isInNarrowPaneModal} = responsiveLayoutValues; + + const {superWideRHPRouteKeys, wideRHPRouteKeys} = useContext(WideRHPContext); + + const isWideRHPDisplayedOnWideLayout = !isSmallScreenWidth && wideRHPRouteKeys.includes(route?.key); + + const isSuperWideRHPDisplayedOnWideLayout = !isSmallScreenWidth && superWideRHPRouteKeys.includes(route?.key); + + const shouldUseNarrowLayout = (isSmallScreenWidth || isInNarrowPaneModal) && !isSuperWideRHPDisplayedOnWideLayout && !isWideRHPDisplayedOnWideLayout; + + return { + ...responsiveLayoutValues, + shouldUseNarrowLayout, + isWideRHPDisplayedOnWideLayout, + isSuperWideRHPDisplayedOnWideLayout, + }; +} diff --git a/src/hooks/useResponsiveLayoutOnWideRHP/types.ts b/src/hooks/useResponsiveLayoutOnWideRHP/types.ts new file mode 100644 index 0000000000000..ca889fb4b263d --- /dev/null +++ b/src/hooks/useResponsiveLayoutOnWideRHP/types.ts @@ -0,0 +1,8 @@ +import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types'; + +type ResponsiveLayoutOnWideRHPResult = ResponsiveLayoutResult & { + isWideRHPDisplayedOnWideLayout: boolean; + isSuperWideRHPDisplayedOnWideLayout: boolean; +}; + +export default ResponsiveLayoutOnWideRHPResult; diff --git a/src/libs/MoneyRequestReportUtils.ts b/src/libs/MoneyRequestReportUtils.ts index 264ecb724d63d..bd3c60d3106c7 100644 --- a/src/libs/MoneyRequestReportUtils.ts +++ b/src/libs/MoneyRequestReportUtils.ts @@ -128,7 +128,11 @@ function shouldDisplayReportTableView(report: OnyxEntry, transactions: T return !isReportTransactionThread(report) && !isSingleTransactionReport(report, transactions); } -function shouldWaitForTransactions(report: OnyxEntry, transactions: Transaction[] | undefined, reportMetadata: OnyxEntry) { +function shouldWaitForTransactions(report: OnyxEntry, transactions: Transaction[] | undefined, reportMetadata: OnyxEntry, isOffline = false) { + if (isOffline) { + return false; + } + const isTransactionDataReady = transactions !== undefined; const isTransactionThreadView = isReportTransactionThread(report); const isStillLoadingData = transactions?.length === 0 && ((!!reportMetadata?.isLoadingInitialReportActions && !reportMetadata.hasOnceLoadedReportActions) || report?.total !== 0); diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 2df277ec6da37..df3c85bebbd4e 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -348,12 +348,7 @@ function AuthScreens() { return; } - if (shouldRenderSecondaryOverlayForRHPOnWideRHP) { - Navigation.dismissToPreviousRHP(); - return; - } - - if (shouldRenderTertiaryOverlay) { + if (shouldRenderSecondaryOverlayForRHPOnWideRHP || shouldRenderTertiaryOverlay) { Navigation.dismissToPreviousRHP(); return; } diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 8e70d36864994..b7135912ab8a5 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -259,6 +259,7 @@ const TaskModalStackNavigator = createModalStackNavigator({ [SCREENS.REPORT_VERIFY_ACCOUNT]: () => require('../../../../pages/home/report/ReportVerifyAccountPage').default, + [SCREENS.EXPENSE_REPORT_VERIFY_ACCOUNT]: () => require('../../../../pages/home/report/ExpenseReportVerifyAccountPage').default, [SCREENS.SEARCH.REPORT_VERIFY_ACCOUNT]: () => require('../../../../pages/Search/SearchReportVerifyAccountPage').default, }); diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index 52d83ca526f64..92cbd4148cf27 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -84,6 +84,7 @@ function SecondaryOverlay() { } const loadRHPReportScreen = () => require('../../../../pages/home/RHPReportScreen').default; +const loadSearchMoneyRequestReportPage = () => require('../../../../pages/Search/SearchMoneyRequestReportPage').default; function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth @@ -343,14 +344,6 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { name={SCREENS.RIGHT_MODAL.SEARCH_REPORT_ACTIONS} component={ModalStackNavigators.SearchReportActionsModalStackNavigator} /> - { - const options = modalStackScreenOptions(props); - return {...options, animation: animationEnabledOnSearchReport ? Animations.SLIDE_FROM_RIGHT : Animations.NONE}; - }} - /> + { + const options = modalStackScreenOptions(props); + return {...options, animation: animationEnabledOnSearchReport ? Animations.SLIDE_FROM_RIGHT : Animations.NONE}; + }} + /> + { + const options = modalStackScreenOptions(props); + return {...options, animation: isSmallScreenWidth ? Animations.SLIDE_FROM_RIGHT : Animations.NONE}; + }} + /> + { + const options = modalStackScreenOptions(props); + return {...options, animation: isSmallScreenWidth ? Animations.SLIDE_FROM_RIGHT : Animations.NONE}; + }} + /> require('@pages/Search/SearchPage').default; -const loadSearchMoneyReportPage = () => require('@pages/Search/SearchMoneyRequestReportPage').default; const Stack = createSearchFullscreenNavigator(); @@ -36,10 +35,6 @@ function SearchFullscreenNavigator({route}: PlatformStackScreenProps - ); diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 9f0bf092e5c60..8166149584f32 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -37,7 +37,16 @@ import setNavigationActionToMicrotaskQueue from './helpers/setNavigationActionTo import {linkingConfig} from './linkingConfig'; import {SPLIT_TO_SIDEBAR} from './linkingConfig/RELATIONS'; import navigationRef from './navigationRef'; -import type {NavigationPartialRoute, NavigationRef, NavigationRoute, NavigationStateRoute, ReportsSplitNavigatorParamList, RootNavigatorParamList, State} from './types'; +import type { + NavigationPartialRoute, + NavigationRef, + NavigationRoute, + NavigationStateRoute, + ReportsSplitNavigatorParamList, + RightModalNavigatorParamList, + RootNavigatorParamList, + State, +} from './types'; // Routes which are part of the flow to set up 2FA const SET_UP_2FA_ROUTES = new Set([ @@ -227,15 +236,20 @@ function navigate(route: Route, options?: LinkToOptions) { } // Start a Sentry span for report navigation - if (route.startsWith('r/') || route.startsWith('search/r/')) { + if (route.startsWith('r/') || route.startsWith('search/r/') || route.startsWith('e/')) { const routePath = Str.cutAfter(route, '?'); - const reportIDMatch = routePath.match(/^(?:search\/)?r\/(\d+)(?:\/\d+)?$/); + const reportIDMatch = route.match(/^(?:search\/)?(?:r|e)\/(\w+)/); const reportID = reportIDMatch?.at(1); if (reportID) { const spanId = `${CONST.TELEMETRY.SPAN_OPEN_REPORT}_${reportID}`; let span = getSpan(spanId); if (!span) { - const spanName = route.startsWith('r/') ? '/r/*' : '/search/r/*'; + let spanName = '/r/*'; + if (route.startsWith('search/r/')) { + spanName = '/search/r/*'; + } else if (route.startsWith('e/')) { + spanName = '/e/*'; + } span = startSpan(spanId, { name: spanName, op: CONST.TELEMETRY.SPAN_OPEN_REPORT, @@ -248,11 +262,9 @@ function navigate(route: Route, options?: LinkToOptions) { }); } } - linkTo(navigationRef.current, route, options); closeSidePanelOnNarrowScreen(); } - /** * When routes are compared to determine whether the fallback route passed to the goUp function is in the state, * these parameters shouldn't be included in the comparison. @@ -596,6 +608,33 @@ function getReportRouteByID(reportID?: string, routes: NavigationRoute[] = navig return null; } +/** + * Get the report ID from the topmost Super Wide RHP modal in the navigation stack. + */ +function getTopmostSuperWideRHPReportID(state: NavigationState = navigationRef.getRootState()): string | undefined { + if (!state) { + return; + } + const topmostRightModalNavigator = state.routes?.at(-1); + + if (!topmostRightModalNavigator || topmostRightModalNavigator.name !== NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { + return; + } + + const topmostSuperWideRHP = topmostRightModalNavigator.state?.routes.findLast((route) => SUPER_WIDE_RIGHT_MODALS.has(route.name)); + + if (!topmostSuperWideRHP) { + return; + } + + const topmostReportParams = topmostSuperWideRHP?.params as + | RightModalNavigatorParamList[typeof SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT] + | RightModalNavigatorParamList[typeof SCREENS.RIGHT_MODAL.EXPENSE_REPORT] + | undefined; + + return topmostReportParams?.reportID; +} + /** * Closes the modal navigator (RHP, onboarding). * @@ -627,8 +666,16 @@ const dismissModal = ({ref = navigationRef, callback}: {ref?: NavigationRef; cal */ const dismissModalWithReport = ({reportID, reportActionID, referrer, backTo}: ReportsSplitNavigatorParamList[typeof SCREENS.REPORT], ref = navigationRef) => { isNavigationReady().then(() => { + const topmostSuperWideRHPReportID = getTopmostSuperWideRHPReportID(); + let areReportsIDsDefined = !!topmostSuperWideRHPReportID && !!reportID; + + if (topmostSuperWideRHPReportID === reportID && areReportsIDsDefined) { + dismissToSuperWideRHP(); + return; + } + const topmostReportID = getTopmostReportId(); - const areReportsIDsDefined = !!topmostReportID && !!reportID; + areReportsIDsDefined = !!topmostReportID && !!reportID; const isReportsSplitTopmostFullScreen = ref.getRootState().routes.findLast((route) => isFullScreenName(route.name))?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR; if (topmostReportID === reportID && areReportsIDsDefined && isReportsSplitTopmostFullScreen) { dismissModal(); @@ -752,7 +799,7 @@ function dismissToModalStack(modalStackNames: Set) { return; } - const lastFoundModalStackIndex = rhpState.routes.findLastIndex((route) => modalStackNames.has(route.name)); + const lastFoundModalStackIndex = rhpState.routes.slice(0, -1).findLastIndex((route) => modalStackNames.has(route.name)); const routesToPop = rhpState.routes.length - lastFoundModalStackIndex - 1; if (routesToPop <= 0 || lastFoundModalStackIndex === -1) { @@ -770,6 +817,10 @@ function dismissToPreviousRHP() { return dismissToModalStack(ALL_WIDE_RIGHT_MODALS); } +function navigateBackToLastSuperWideRHPScreen() { + return dismissToModalStack(SUPER_WIDE_RIGHT_MODALS); +} + function dismissToSuperWideRHP() { // On narrow layouts (mobile), Super Wide RHP doesn't exist, so just dismiss the modal completely if (getIsNarrowLayout()) { @@ -777,7 +828,7 @@ function dismissToSuperWideRHP() { return; } // On wide layouts, dismiss back to the Super Wide RHP modal stack - return dismissToModalStack(SUPER_WIDE_RIGHT_MODALS); + navigateBackToLastSuperWideRHPScreen(); } function getTopmostReportIDInSearchRHP(state = navigationRef.getRootState()): string | undefined { @@ -837,6 +888,8 @@ export default { dismissToPreviousRHP, dismissToSuperWideRHP, getTopmostReportIDInSearchRHP, + getTopmostSuperWideRHPReportID, + navigateBackToLastSuperWideRHPScreen, }; export {navigationRef}; diff --git a/src/libs/Navigation/helpers/isReportOpenInRHP.ts b/src/libs/Navigation/helpers/isReportOpenInRHP.ts index 51e8a95bb66bd..fde83327e71bd 100644 --- a/src/libs/Navigation/helpers/isReportOpenInRHP.ts +++ b/src/libs/Navigation/helpers/isReportOpenInRHP.ts @@ -1,4 +1,5 @@ import type {NavigationState} from '@react-navigation/native'; +import {ALL_WIDE_RIGHT_MODALS} from '@components/WideRHPContextProvider/WIDE_RIGHT_MODALS'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; @@ -11,7 +12,7 @@ const isReportOpenInRHP = (state: NavigationState | undefined): boolean => { if (params && 'screen' in params && typeof params.screen === 'string' && params.screen === SCREENS.RIGHT_MODAL.SEARCH_REPORT) { return true; } - return !!(lastRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && lastRoute.state?.routes?.some((route) => route?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT)); + return !!(lastRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && lastRoute.state?.routes?.some((route) => ALL_WIDE_RIGHT_MODALS.has(route.name))); }; export default isReportOpenInRHP; diff --git a/src/libs/Navigation/helpers/isReportOpenInSuperWideRHP.ts b/src/libs/Navigation/helpers/isReportOpenInSuperWideRHP.ts new file mode 100644 index 0000000000000..8ddf6ba84aa11 --- /dev/null +++ b/src/libs/Navigation/helpers/isReportOpenInSuperWideRHP.ts @@ -0,0 +1,13 @@ +import type {NavigationState} from '@react-navigation/native'; +import {SUPER_WIDE_RIGHT_MODALS} from '@components/WideRHPContextProvider/WIDE_RIGHT_MODALS'; +import NAVIGATORS from '@src/NAVIGATORS'; + +const isReportOpenInSuperWideRHP = (state: NavigationState | undefined): boolean => { + const lastRoute = state?.routes?.at(-1); + if (!lastRoute) { + return false; + } + return !!(lastRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && lastRoute.state?.routes?.some((route) => SUPER_WIDE_RIGHT_MODALS.has(route.name))); +}; + +export default isReportOpenInSuperWideRHP; diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts index b6ba33cb8c56f..de08480ebd507 100644 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SEARCH_TO_RHP.ts @@ -50,6 +50,8 @@ const SEARCH_TO_RHP: Partial['config'] = { [SCREENS.RIGHT_MODAL.REPORT_VERIFY_ACCOUNT]: { screens: { [SCREENS.REPORT_VERIFY_ACCOUNT]: ROUTES.REPORT_VERIFY_ACCOUNT.route, + [SCREENS.EXPENSE_REPORT_VERIFY_ACCOUNT]: ROUTES.EXPENSE_REPORT_VERIFY_ACCOUNT.route, [SCREENS.SEARCH.REPORT_VERIFY_ACCOUNT]: ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.route, }, }, @@ -1870,6 +1871,8 @@ const config: LinkingOptions['config'] = { [SCREENS.REPORT_CHANGE_APPROVER.ADD_APPROVER]: ROUTES.REPORT_CHANGE_APPROVER_ADD_APPROVER.route, }, }, + [SCREENS.RIGHT_MODAL.EXPENSE_REPORT]: ROUTES.EXPENSE_REPORT_RHP.route, + [SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT]: ROUTES.SEARCH_MONEY_REQUEST_REPORT.route, [SCREENS.RIGHT_MODAL.DOMAIN]: { screens: { [SCREENS.WORKSPACES_VERIFY_DOMAIN]: { @@ -2035,9 +2038,6 @@ const config: LinkingOptions['config'] = { [SCREENS.SEARCH.ROOT]: { path: ROUTES.SEARCH_ROOT.route, }, - [SCREENS.SEARCH.MONEY_REQUEST_REPORT]: { - path: ROUTES.SEARCH_MONEY_REQUEST_REPORT.route, - }, }, }, [NAVIGATORS.SHARE_MODAL_NAVIGATOR]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 7854b8717bcd2..182b2932caabd 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -88,6 +88,9 @@ type ReportVerifyAccountNavigatorParamList = { [SCREENS.REPORT_VERIFY_ACCOUNT]: { reportID: string; }; + [SCREENS.EXPENSE_REPORT_VERIFY_ACCOUNT]: { + reportID: string; + }; }; type SettingsNavigatorParamList = { @@ -2252,14 +2255,24 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.SCHEDULE_CALL]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.REPORT_CHANGE_APPROVER]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.MERGE_TRANSACTION]: NavigatorScreenParams; - [SCREENS.RIGHT_MODAL.DOMAIN]: NavigatorScreenParams; - [SCREENS.RIGHT_MODAL.SEARCH_COLUMNS]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.EXPENSE_REPORT]: { + reportID: string; + // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md + backTo?: Routes; + }; + [SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT]: { + reportID: string; + // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md + backTo?: Routes; + }; [SCREENS.RIGHT_MODAL.SEARCH_REPORT]: { reportID: string; reportActionID?: string; // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md backTo?: Routes; }; + [SCREENS.RIGHT_MODAL.DOMAIN]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.SEARCH_COLUMNS]: NavigatorScreenParams; }; type TravelNavigatorParamList = { @@ -2789,11 +2802,6 @@ type SearchFullscreenNavigatorParamList = { name?: string; groupBy?: string; }; - [SCREENS.SEARCH.MONEY_REQUEST_REPORT]: { - reportID: string; - // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md - backTo?: Routes; - }; }; type SearchAdvancedFiltersParamList = { diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts index 6f9194ac60651..435468cb8a0e4 100644 --- a/src/libs/ReportActionComposeFocusManager.ts +++ b/src/libs/ReportActionComposeFocusManager.ts @@ -42,7 +42,7 @@ function focus(shouldFocusForNonBlurInputOnTapOutside?: boolean) { /** Do not trigger the refocusing when the active route is not the report screen */ const navigationState = navigationRef.getState(); const focusedRoute = findFocusedRoute(navigationState); - if (!navigationState || (!isReportOpenInRHP(navigationState) && focusedRoute?.name !== SCREENS.REPORT && focusedRoute?.name !== SCREENS.SEARCH.MONEY_REQUEST_REPORT)) { + if (!navigationState || (!isReportOpenInRHP(navigationState) && focusedRoute?.name !== SCREENS.REPORT)) { return; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 416ac9d0430ee..9d3090efe320d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6201,7 +6201,7 @@ function goBackToDetailsPage(report: OnyxEntry, backTo?: string, shouldG } } -function navigateBackOnDeleteTransaction(backRoute: Route | undefined, isFromRHP?: boolean) { +function navigateBackOnDeleteTransaction(backRoute: Route | undefined) { if (!backRoute) { return; } @@ -6209,25 +6209,10 @@ function navigateBackOnDeleteTransaction(backRoute: Route | undefined, isFromRHP const rootState = navigationRef.current?.getRootState(); const lastFullScreenRoute = rootState?.routes.findLast((route) => isFullScreenName(route.name)); if (lastFullScreenRoute?.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR) { - const searchFullScreenRoutes = rootState?.routes.findLast((route) => route.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR); - const lastRoute = searchFullScreenRoutes?.state?.routes?.at(-1); - if (lastRoute?.name === SCREENS.SEARCH.MONEY_REQUEST_REPORT) { - const lastRouteParams = lastRoute?.params; - const newBackRoute = lastRouteParams && 'backTo' in lastRouteParams ? lastRouteParams?.backTo : undefined; - if (isFromRHP) { - Navigation.dismissModal(); - } - Navigation.isNavigationReady().then(() => { - Navigation.goBack(newBackRoute as Route); - }); - return; - } - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); return; } - if (isFromRHP) { - Navigation.dismissModal(); - } + Navigation.dismissToSuperWideRHP(); Navigation.isNavigationReady().then(() => { Navigation.goBack(backRoute); }); diff --git a/src/libs/SettlementButtonUtils.ts b/src/libs/SettlementButtonUtils.ts index 246f9d985676c..34d870abb86cc 100644 --- a/src/libs/SettlementButtonUtils.ts +++ b/src/libs/SettlementButtonUtils.ts @@ -43,6 +43,10 @@ const getRouteMappings = (chatReportID: string, reportID?: string): RouteMapping check: (activeRoute: string) => activeRoute.includes(ROUTES.SEARCH_REPORT.getRoute({reportID: chatReportID})), navigate: () => Navigation.navigate(ROUTES.SEARCH_REPORT_VERIFY_ACCOUNT.getRoute(chatReportID)), }, + { + check: (activeRoute: string) => activeRoute.includes(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: chatReportID})), + navigate: () => Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(chatReportID)), + }, ]; if (reportID === undefined) { @@ -62,6 +66,10 @@ const getRouteMappings = (chatReportID: string, reportID?: string): RouteMapping check: (activeRoute: string) => activeRoute.includes(ROUTES.REPORT_WITH_ID.getRoute(reportID)), navigate: () => Navigation.navigate(ROUTES.REPORT_VERIFY_ACCOUNT.getRoute(reportID)), }, + { + check: (activeRoute: string) => activeRoute.includes(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID})), + navigate: () => Navigation.navigate(ROUTES.EXPENSE_REPORT_VERIFY_ACCOUNT.getRoute(reportID)), + }, ]; return [...nonReportIdRouteMappings, ...reportIdRouteMappings]; diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index eaee8f4c9ca9b..ac83a12874d2f 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -64,6 +64,7 @@ import * as Localize from '@libs/Localize'; import Log from '@libs/Log'; import {validateAmount} from '@libs/MoneyRequestUtils'; import isReportOpenInRHP from '@libs/Navigation/helpers/isReportOpenInRHP'; +import isReportOpenInSuperWideRHP from '@libs/Navigation/helpers/isReportOpenInSuperWideRHP'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import {isOffline} from '@libs/Network/NetworkStore'; @@ -989,6 +990,11 @@ function dismissModalAndOpenReportInInboxTab(reportID?: string) { const rhpKey = rootState.routes.at(-1)?.state?.key; if (rhpKey) { const hasMultipleTransactions = Object.values(allTransactions).filter((transaction) => transaction?.reportID === reportID).length > 0; + // When a report is opened in the super wide RHP, we need to dismiss to the first RHP to show the same report with new expense. + if (isReportOpenInSuperWideRHP(rootState)) { + Navigation.dismissToPreviousRHP(); + return; + } // When a report with one expense is opened in the wide RHP and the user adds another expense, RHP should be dismissed and ROUTES.SEARCH_MONEY_REQUEST_REPORT should be displayed. if (hasMultipleTransactions && reportID) { Navigation.dismissModal(); @@ -13885,7 +13891,7 @@ function updateSplitTransactionsFromSplitExpensesFlow(params: UpdateSplitTransac } if (isSearchPageTopmostFullScreenRoute || !transactionReport?.parentReportID) { - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); // After the modal is dismissed, remove the transaction thread report screen // to avoid navigating back to a report removed by the split transaction. diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 39c1d867ae722..8bd789cffdb01 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -72,12 +72,13 @@ import getEnvironment from '@libs/Environment/getEnvironment'; import type EnvironmentType from '@libs/Environment/getEnvironment/types'; import {getMicroSecondOnyxErrorWithTranslationKey, getMicroSecondTranslationErrorWithTranslationKey} from '@libs/ErrorUtils'; import fileDownload from '@libs/fileDownload'; +import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import HttpUtils from '@libs/HttpUtils'; import Log from '@libs/Log'; import {isEmailPublicDomain} from '@libs/LoginUtils'; import {getMovedReportID} from '@libs/ModifiedExpenseMessage'; import type {LinkToOptions} from '@libs/Navigation/helpers/linkTo/types'; -import Navigation from '@libs/Navigation/Navigation'; +import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import enhanceParameters from '@libs/Network/enhanceParameters'; import * as NetworkStore from '@libs/Network/NetworkStore'; import NetworkConnection from '@libs/NetworkConnection'; @@ -181,6 +182,8 @@ import type {OnboardingAccounting} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Route} from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/NewRoomForm'; import type { BankAccountList, @@ -1974,12 +1977,35 @@ function handlePreexistingReport(report: Report) { }); Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, null); }; + + if (!navigationRef.isReady()) { + callback(); + return; + } + + const activeRouteInfo = navigationRef.getCurrentRoute(); + const backTo = (activeRouteInfo?.params as {backTo?: Route})?.backTo; + const screenName = activeRouteInfo?.name; + const activeRoute = activeRouteInfo?.path; + + const isOptimisticReportFocused = activeRoute?.includes(`/r/${reportID}`); + + // Fix specific case: https://github.com/Expensify/App/pull/77657#issuecomment-3678696730. + // When user is editing a money request report (/e/:reportID route) and has + // an optimistic report in the background that should be replaced with preexisting report + const isOptimisticReportInBackground = screenName === SCREENS.RIGHT_MODAL.EXPENSE_REPORT && backTo && backTo.includes(`/r/${reportID}`); + // Only re-route them if they are still looking at the optimistically created report - if (Navigation.getActiveRoute().includes(`/r/${reportID}`)) { + if (isOptimisticReportFocused || isOptimisticReportInBackground) { const currCallback = callback; callback = () => { currCallback(); - Navigation.setParams({reportID: preexistingReportID.toString()}); + if (isOptimisticReportFocused) { + Navigation.setParams({reportID: preexistingReportID.toString()}); + } else if (isOptimisticReportInBackground) { + // Navigate to the correct backTo route with the preexisting report ID + Navigation.navigate(backTo.replace(`/r/${reportID}`, `/r/${preexistingReportID}`) as Route); + } }; // The report screen will listen to this event and transfer the draft comment to the existing report @@ -3692,8 +3718,15 @@ function navigateToMostRecentReport(currentReport: OnyxEntry) { const lastAccessedReportID = findLastAccessedReport(false, false, undefined, currentReport?.reportID)?.reportID; if (lastAccessedReportID) { - const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID); - Navigation.goBack(lastAccessedReportRoute); + // Check if route exists for super wide RHP vs regular full screen report + const topmostSuperWideRHP = Navigation.getTopmostSuperWideRHPReportID(); + + if (lastAccessedReportID === topmostSuperWideRHP && !getIsNarrowLayout()) { + Navigation.dismissToSuperWideRHP(); + } else { + const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID); + Navigation.goBack(lastAccessedReportRoute); + } } else { const isChatThread = isChatThreadReportUtils(currentReport); diff --git a/src/pages/ReportAddApproverPage.tsx b/src/pages/ReportAddApproverPage.tsx index dedcb86320794..61cff123d3e03 100644 --- a/src/pages/ReportAddApproverPage.tsx +++ b/src/pages/ReportAddApproverPage.tsx @@ -100,7 +100,7 @@ function ReportAddApproverPage({report, isLoadingReportData, policy}: ReportAddA isASAPSubmitBetaEnabled, reportNextStep, ); - Navigation.dismissModal(); + Navigation.dismissToPreviousRHP(); }, [allApprovers, selectedApproverEmail, report, currentUserDetails.accountID, currentUserDetails.email, policy, hasViolations, isASAPSubmitBetaEnabled, reportNextStep]); const button = useMemo(() => { diff --git a/src/pages/ReportChangeApproverPage.tsx b/src/pages/ReportChangeApproverPage.tsx index d9ebe90b0208e..7b6bd438ac7c2 100644 --- a/src/pages/ReportChangeApproverPage.tsx +++ b/src/pages/ReportChangeApproverPage.tsx @@ -73,8 +73,8 @@ function ReportChangeApproverPage({report, policy, isLoadingReportData}: ReportC return; } assignReportToMe(report, currentUserDetails.accountID, currentUserDetails.email ?? '', policy, hasViolations, isASAPSubmitBetaEnabled, reportNextStep); - Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(reportID)); - }, [selectedApproverType, report, currentUserDetails.accountID, currentUserDetails.email, policy, hasViolations, isASAPSubmitBetaEnabled, reportNextStep, reportID]); + Navigation.dismissToPreviousRHP(); + }, [selectedApproverType, report, currentUserDetails.accountID, currentUserDetails.email, policy, hasViolations, isASAPSubmitBetaEnabled, reportNextStep]); const approverTypes = useMemo(() => { const data: Array> = [ diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 87dbfad34d2fa..9b25bd252c53e 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -1,3 +1,4 @@ +import {StackActions} from '@react-navigation/native'; import reportsSelector from '@selectors/Attributes'; import {Str} from 'expensify-common'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; @@ -22,6 +23,7 @@ import RoomHeaderAvatars from '@components/RoomHeaderAvatars'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import {useSearchContext} from '@components/Search/SearchContext'; +import {SUPER_WIDE_RIGHT_MODALS} from '@components/WideRHPContextProvider/WIDE_RIGHT_MODALS'; import useActivePolicy from '@hooks/useActivePolicy'; import useAncestors from '@hooks/useAncestors'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -41,9 +43,9 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import getBase62ReportID from '@libs/getBase62ReportID'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; -import Navigation from '@libs/Navigation/Navigation'; +import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {ReportDetailsNavigatorParamList} from '@libs/Navigation/types'; +import type {ReportDetailsNavigatorParamList, RightModalNavigatorParamList} from '@libs/Navigation/types'; import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; import Parser from '@libs/Parser'; import Permissions from '@libs/Permissions'; @@ -123,7 +125,7 @@ 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 SCREENS from '@src/SCREENS'; +import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -327,7 +329,6 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail }, [report?.reportID, isOffline, isPrivateNotesFetchTriggered, isSelfDM]); const leaveChat = useCallback(() => { - Navigation.dismissModal(); Navigation.isNavigationReady().then(() => { if (isRootGroupChat) { leaveGroupChat(report.reportID, quickAction?.chatReportID?.toString() === report.reportID, currentUserPersonalDetails.accountID); @@ -903,9 +904,39 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail // Where to navigate back to after deleting the transaction and its report. const navigateToTargetUrl = useCallback(() => { let urlToNavigateBack: string | undefined; - // Only proceed with navigation logic if transaction was actually deleted if (!isEmptyObject(requestParentReportAction)) { + const rootState = navigationRef.getRootState(); + const rhp = rootState.routes.at(-1); + const rhpRoutes = rhp?.state?.routes ?? []; + const previousRoute = rhpRoutes.at(-2); + const superWideRHPIndex = rhpRoutes.findIndex((rhpRoute) => SUPER_WIDE_RIGHT_MODALS.has(rhpRoute.name)); + + // If the deleted expense is displayed directly below, close the entire RHP + const isSuperWideRHPDisplayed = superWideRHPIndex > -1; + const isSuperWideRHPDisplayedDirectlyBelow = isSuperWideRHPDisplayed && superWideRHPIndex === rhpRoutes.length - 2; + if ( + isSuperWideRHPDisplayedDirectlyBelow && + (previousRoute?.params as RightModalNavigatorParamList[typeof SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT])?.reportID === route.params.reportID + ) { + Navigation.dismissModal(); + return; + } + + // If the deleted expense is opened from the super wide rhp, go back there. + if ( + previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT && + (previousRoute.params as RightModalNavigatorParamList[typeof SCREENS.RIGHT_MODAL.SEARCH_REPORT])?.reportID === route.params.reportID + ) { + if (isSuperWideRHPDisplayed) { + const distanceToPop = rhpRoutes.length - 1 - superWideRHPIndex; + navigationRef.dispatch({...StackActions.pop(distanceToPop), target: rhp?.state?.key}); + return; + } + Navigation.dismissModal(); + return; + } + const isTrackExpense = isTrackExpenseAction(requestParentReportAction); if (isTrackExpense) { urlToNavigateBack = getNavigationUrlAfterTrackExpenseDelete( @@ -934,9 +965,9 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail Navigation.dismissModal(); } else { setDeleteTransactionNavigateBackUrl(urlToNavigateBack); - navigateBackOnDeleteTransaction(urlToNavigateBack as Route, true); + navigateBackOnDeleteTransaction(urlToNavigateBack as Route); } - }, [iouTransactionID, requestParentReportAction, isSingleTransactionView, moneyRequestReport, isChatIOUReportArchived, iouReport, chatIOUReport]); + }, [requestParentReportAction, route.params.reportID, moneyRequestReport, iouTransactionID, iouReport, chatIOUReport, isChatIOUReportArchived, isSingleTransactionView]); // A flag to indicate whether the user chose to delete the transaction or not const isTransactionDeleted = useRef(false); diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index cfeb0082a01d2..edc5da00af3b1 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -1,13 +1,15 @@ import {PortalHost} from '@gorhom/portal'; +import {useIsFocused} from '@react-navigation/native'; import React, {useEffect, useMemo, useRef, useState} from 'react'; import type {FlatList} from 'react-native'; -import {View} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; import MoneyRequestReportView from '@components/MoneyRequestReportView/MoneyRequestReportView'; import ScreenWrapper from '@components/ScreenWrapper'; import {useSearchContext} from '@components/Search/SearchContext'; +import useShowSuperWideRHPVersion from '@components/WideRHPContextProvider/useShowSuperWideRHPVersion'; +import WideRHPOverlayWrapper from '@components/WideRHPOverlayWrapper'; import useIsReportReadyToDisplay from '@hooks/useIsReportReadyToDisplay'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; @@ -19,7 +21,7 @@ import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViol import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; +import type {RightModalNavigatorParamList} from '@libs/Navigation/types'; import { getFilteredReportActionsForReportView, getIOUActionForTransactionID, @@ -31,16 +33,18 @@ import { import {isValidReportIDFromPath} from '@libs/ReportUtils'; import Navigation from '@navigation/Navigation'; import ReactionListWrapper from '@pages/home/ReactionListWrapper'; -import {createTransactionThreadReport, openReport} from '@userActions/Report'; +import {createTransactionThreadReport, openReport, updateLastVisitTime} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ActionListContextType, ScrollPosition} from '@src/pages/home/ReportScreenContext'; import {ActionListContext} from '@src/pages/home/ReportScreenContext'; -import type SCREENS from '@src/SCREENS'; +import SCREENS from '@src/SCREENS'; import type {Policy, Transaction, TransactionViolations} from '@src/types/onyx'; import {getEmptyObject} from '@src/types/utils/EmptyObject'; -type SearchMoneyRequestPageProps = PlatformStackScreenProps; +type SearchMoneyRequestPageProps = + | PlatformStackScreenProps + | PlatformStackScreenProps; const defaultReportMetadata = { isLoadingInitialReportActions: true, @@ -55,13 +59,23 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); const {isOffline} = useNetwork(); - const reportIDFromRoute = getNonEmptyStringOnyxID(route.params?.reportID); const {currentSearchHash} = useSearchContext(); const [snapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchHash}`, {canBeMissing: true}); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`, {allowStaleData: true, canBeMissing: true}); + const isFocused = useIsFocused(); + + useEffect(() => { + // Update last visit time when the expense super wide RHP report is focused + if (!reportIDFromRoute || !isFocused || route.name !== SCREENS.RIGHT_MODAL.EXPENSE_REPORT) { + return; + } + + updateLastVisitTime(reportIDFromRoute); + }, [reportIDFromRoute, isFocused, route.name]); + const snapshotReport = useMemo(() => { // eslint-disable-next-line @typescript-eslint/no-deprecated return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`] ?? {}) as typeof report; @@ -124,6 +138,11 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { return {snapshotTransaction: transaction, snapshotViolations: violations}; }, [snapshot?.data, allReportTransactions]); + // If there is more than one transaction, display the report in Super Wide RHP, otherwise it will be shown in Wide RHP + const shouldShowSuperWideRHP = visibleTransactions.length > 1; + + useShowSuperWideRHPVersion(shouldShowSuperWideRHP); + useEffect(() => { if (transactionThreadReportID === CONST.FAKE_REPORT_ID && oneTransactionID) { const iouAction = getIOUActionForTransactionID(reportActions, oneTransactionID); @@ -210,8 +229,8 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { [reportID, reportMetadata?.isLoadingInitialReportActions], ); - if (shouldUseNarrowLayout) { - return ( + return ( + + - ); - } - - return ( - - - - - - - - - - - - - - - - + ); } diff --git a/src/pages/TransactionDuplicate/Confirmation.tsx b/src/pages/TransactionDuplicate/Confirmation.tsx index a923c5c75b126..eba4c10cdc0d2 100644 --- a/src/pages/TransactionDuplicate/Confirmation.tsx +++ b/src/pages/TransactionDuplicate/Confirmation.tsx @@ -1,5 +1,5 @@ import {useRoute} from '@react-navigation/native'; -import React, {useCallback, useMemo} from 'react'; +import React, {useCallback, useContext, useMemo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -12,6 +12,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import {ShowContextMenuContext} from '@components/ShowContextMenuContext'; import Text from '@components/Text'; +import {WideRHPContext} from '@components/WideRHPContextProvider'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -64,6 +65,8 @@ function Confirmation() { const reportAction = Object.values(reportActions ?? {}).find( (action) => ReportActionsUtils.isMoneyRequestAction(action) && ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID === reviewDuplicates?.transactionID, ); + const {superWideRHPRouteKeys} = useContext(WideRHPContext); + const isSuperWideRHPDisplayed = superWideRHPRouteKeys.length > 0; const [duplicates] = useTransactionsByID(reviewDuplicates?.duplicates); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`, {canBeMissing: true}); @@ -79,12 +82,16 @@ function Confirmation() { transactionsMergeParams.transactionThreadReportID = transactionThreadReportID; } mergeDuplicates(transactionsMergeParams); + if (isSuperWideRHPDisplayed) { + Navigation.dismissToSuperWideRHP(); + return; + } Navigation.dismissModal(); - }, [reportAction?.childReportID, transactionsMergeParams]); + }, [reportAction?.childReportID, transactionsMergeParams, isSuperWideRHPDisplayed]); const handleResolveDuplicates = useCallback(() => { resolveDuplicates(transactionsMergeParams); - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); }, [transactionsMergeParams]); const contextValue = useMemo( diff --git a/src/pages/TransactionMerge/ConfirmationPage.tsx b/src/pages/TransactionMerge/ConfirmationPage.tsx index 72a673fb5fb3d..488edf28ff15d 100644 --- a/src/pages/TransactionMerge/ConfirmationPage.tsx +++ b/src/pages/TransactionMerge/ConfirmationPage.tsx @@ -103,7 +103,7 @@ function ConfirmationPage({route}: ConfirmationPageProps) { Navigation.dismissModalWithReport({reportID: reportIDToDismiss}); } } else { - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); } }; diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index e6a752a84ed46..967c220613ff3 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -357,7 +357,7 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr const isMoneyRequestOrInvoiceReport = isMoneyRequestReport(report) || isInvoiceReport(report); // Prevent the empty state flash by ensuring transaction data is fully loaded before deciding which view to render // We need to wait for both the selector to finish AND ensure we're not in a loading state where transactions could still populate - const shouldWaitForTransactions = !isOffline && shouldWaitForTransactionsUtil(report, reportTransactions, reportMetadata); + const shouldWaitForTransactions = shouldWaitForTransactionsUtil(report, reportTransactions, reportMetadata, isOffline); const newTransactions = useNewTransactions(reportMetadata?.hasOnceLoadedReportActions, reportTransactions); @@ -394,7 +394,7 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr return; } if (isInNarrowPaneModal) { - Navigation.dismissModal(); + Navigation.goBack(); return; } if (backTo) { diff --git a/src/pages/home/report/ExpenseReportVerifyAccountPage.tsx b/src/pages/home/report/ExpenseReportVerifyAccountPage.tsx new file mode 100644 index 0000000000000..19ad75760b6dd --- /dev/null +++ b/src/pages/home/report/ExpenseReportVerifyAccountPage.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {ReportVerifyAccountNavigatorParamList} from '@libs/Navigation/types'; +import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type ExpenseReportVerifyAccountPageProps = PlatformStackScreenProps; + +function ExpenseReportVerifyAccountPage({route}: ExpenseReportVerifyAccountPageProps) { + return ; +} + +export default ExpenseReportVerifyAccountPage; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 0a97361266789..632cec2cf8295 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -1,7 +1,7 @@ -import {useIsFocused, useNavigation} from '@react-navigation/native'; +import {useIsFocused, useNavigation, useRoute} from '@react-navigation/native'; import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef, RefObject} from 'react'; -import React, {memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import React, {memo, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type {BlurEvent, LayoutChangeEvent, MeasureInWindowOnSuccessCallback, TextInput, TextInputContentSizeChangeEvent, TextInputKeyPressEvent, TextInputScrollEvent} from 'react-native'; import {DeviceEventEmitter, InteractionManager, NativeModules, StyleSheet, View} from 'react-native'; import {useFocusedInputHandler} from 'react-native-keyboard-controller'; @@ -11,6 +11,7 @@ import type {Emoji} from '@assets/emojis/types'; import type {MeasureParentContainerAndCursorCallback} from '@components/AutoCompleteSuggestions/types'; import Composer from '@components/Composer'; import type {CustomSelectionChangeEvent, TextSelection} from '@components/Composer/types'; +import {WideRHPContext} from '@components/WideRHPContextProvider'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; @@ -49,6 +50,7 @@ import {areAllModalsHidden} from '@userActions/Modal'; import {broadcastUserIsTyping, saveReportActionDraft, saveReportDraftComment} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type {FileObject} from '@src/types/utils/Attachment'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; @@ -232,6 +234,7 @@ function ComposerWithSuggestions({ // Fullstory forwardedFSClass, }: ComposerWithSuggestionsProps) { + const route = useRoute(); const {isKeyboardShown} = useKeyboardState(); const theme = useTheme(); const styles = useThemeStyles(); @@ -255,6 +258,10 @@ function ComposerWithSuggestions({ const commentRef = useRef(value); + const {superWideRHPRouteKeys} = useContext(WideRHPContext); + // Autofocus is disabled on SearchReport when another RHP is displayed below as it causes animation issues + const shouldDisableAutoFocus = superWideRHPRouteKeys.length > 0 && route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT; + const [modal] = useOnyx(ONYXKEYS.MODAL, {canBeMissing: true}); const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {canBeMissing: true}); const [editFocused] = useOnyx(ONYXKEYS.INPUT_FOCUSED, {canBeMissing: true}); @@ -266,7 +273,8 @@ function ComposerWithSuggestions({ const {shouldUseNarrowLayout} = useResponsiveLayout(); const maxComposerLines = shouldUseNarrowLayout ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; - const shouldAutoFocus = (shouldFocusInputOnScreenFocus || !!draftComment) && shouldShowComposeInput && areAllModalsHidden() && isFocused && !didHideComposerInput; + const shouldAutoFocus = + (shouldFocusInputOnScreenFocus || !!draftComment) && shouldShowComposeInput && areAllModalsHidden() && isFocused && !didHideComposerInput && !shouldDisableAutoFocus; const valueRef = useRef(value); valueRef.current = value; diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 367db0380bfeb..3463b28b2a247 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -624,13 +624,7 @@ function ReportActionsList({ setIsFloatingMessageCounterVisible(false); if (!hasNewestReportAction) { - if (isSearchTopmostFullScreenRoute()) { - if (Navigation.getReportRHPActiveRoute()) { - Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: report.reportID, backTo})); - } else { - Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: report.reportID, backTo})); - } - } else { + if (!Navigation.getReportRHPActiveRoute()) { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID, undefined, undefined, backTo)); } openReport(report.reportID); diff --git a/src/pages/iou/RejectReasonPage.tsx b/src/pages/iou/RejectReasonPage.tsx index 929cbb58347dd..1068c65e9c0fe 100644 --- a/src/pages/iou/RejectReasonPage.tsx +++ b/src/pages/iou/RejectReasonPage.tsx @@ -5,6 +5,7 @@ import {useSearchContext} from '@components/Search/SearchContext'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; +import getIsSmallScreenWidth from '@libs/getIsSmallScreenWidth'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -32,8 +33,8 @@ function RejectReasonPage({route}: RejectReasonPageProps) { const onSubmit = (values: FormOnyxValues) => { const urlToNavigateBack = rejectMoneyRequest(transactionID, reportID, values.comment, policy); removeTransaction(transactionID); - Navigation.dismissModal(); - if (urlToNavigateBack) { + Navigation.dismissToSuperWideRHP(); + if (urlToNavigateBack && getIsSmallScreenWidth()) { Navigation.isNavigationReady().then(() => Navigation.goBack(urlToNavigateBack)); } }; diff --git a/src/pages/iou/SplitExpensePage.tsx b/src/pages/iou/SplitExpensePage.tsx index b1c36fe24f97b..bbecb865ee2ee 100644 --- a/src/pages/iou/SplitExpensePage.tsx +++ b/src/pages/iou/SplitExpensePage.tsx @@ -163,12 +163,12 @@ function SplitExpensePage({route}: SplitExpensePageProps) { const splitExpenseWithoutID = {...splitExpenses.at(0), transactionID: ''}; // When we try to save one split during splits creation and if the data is identical to the original transaction we should close the split flow if (!childTransactions.length && deepEqual(splitFieldDataFromOriginalTransactionWithoutID, splitExpenseWithoutID)) { - Navigation.dismissModal(); + Navigation.dismissToPreviousRHP(); return; } // When we try to save splits during editing splits and if the data is identical to the already created transactions we should close the split flow if (childTransactions.length && deepEqual(splitFieldDataFromChildTransactions, splitExpenses)) { - Navigation.dismissModal(); + Navigation.dismissToPreviousRHP(); return; } // When we try to save one split during splits creation and if the data is not identical to the original transaction we should show the error @@ -196,7 +196,7 @@ function SplitExpensePage({route}: SplitExpensePageProps) { // When we try to save splits during editing splits and if the data is identical to the already created transactions we should close the split flow if (deepEqual(splitFieldDataFromChildTransactions, splitExpenses)) { - Navigation.dismissModal(); + Navigation.dismissToPreviousRHP(); return; } diff --git a/src/pages/iou/request/step/IOURequestEditReport.tsx b/src/pages/iou/request/step/IOURequestEditReport.tsx index 03ee0e7fe4c99..f059f2887ccf0 100644 --- a/src/pages/iou/request/step/IOURequestEditReport.tsx +++ b/src/pages/iou/request/step/IOURequestEditReport.tsx @@ -57,7 +57,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { const selectReport = (item: TransactionGroupListItem, report?: OnyxEntry) => { if (selectedTransactionIDs.length === 0 || item.value === reportID) { - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); return; } @@ -79,7 +79,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { clearSelectedTransactions(true); }); - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); }; const removeFromReport = () => { @@ -97,7 +97,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { turnOffMobileSelectionMode(); } clearSelectedTransactions(true); - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); }; const createReportForPolicy = (shouldDismissEmptyReportsConfirmation?: boolean) => { diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx index b9625fd07474c..1c413dfd0aa50 100644 --- a/src/pages/iou/request/step/IOURequestStepReport.tsx +++ b/src/pages/iou/request/step/IOURequestStepReport.tsx @@ -68,7 +68,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { const handleGoBack = () => { if (isEditing) { - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); } else { Navigation.goBack(backTo); } @@ -167,7 +167,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { if (!transaction) { return; } - Navigation.dismissModal(); + Navigation.dismissToSuperWideRHP(); // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { changeTransactionsReport({ diff --git a/src/styles/index.ts b/src/styles/index.ts index 1f56011feb3cd..7506658f08659 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -7,8 +7,8 @@ import type {LineLayer} from 'react-map-gl'; import type {Animated, ImageStyle, TextStyle, ViewStyle} from 'react-native'; import {Platform, StyleSheet} from 'react-native'; import type {PickerStyle} from 'react-native-picker-select'; -import {interpolate} from 'react-native-reanimated'; import type {SharedValue} from 'react-native-reanimated'; +import {interpolate} from 'react-native-reanimated'; import type {MixedStyleDeclaration, MixedStyleRecord} from 'react-native-render-html'; import type {ValueOf} from 'type-fest'; import type DotLottieAnimation from '@components/LottieAnimations/types'; diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 9362e696b1168..a934102a8c05e 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -110,6 +110,7 @@ export default { chooseFilesViewMargin: 8, sideBarWithLHBWidth: 320, navigationTabBarSize: 72, + popoverMargin: 18, pdfPageMaxWidth: 992, tooltipZIndex: 10050, gutterWidth: 12, diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index fa9cfcf39136c..b33dbfc5d96fb 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -110,6 +110,8 @@ const topMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), dismissModal: jest.fn(), + dismissToPreviousRHP: jest.fn(), + dismissToSuperWideRHP: jest.fn(), dismissModalWithReport: jest.fn(), goBack: jest.fn(), getTopmostReportId: jest.fn(() => topMostReportID), diff --git a/tests/perf-test/ReportActionCompose.perf-test.tsx b/tests/perf-test/ReportActionCompose.perf-test.tsx index 92c7b77456e87..4496f145ef57c 100644 --- a/tests/perf-test/ReportActionCompose.perf-test.tsx +++ b/tests/perf-test/ReportActionCompose.perf-test.tsx @@ -48,6 +48,7 @@ jest.mock('@react-navigation/native', () => { useIsFocused: () => true, useNavigationState: () => {}, useFocusEffect: jest.fn(), + useRoute: () => jest.fn(), }; }); diff --git a/tests/utils/TestNavigationContainer.tsx b/tests/utils/TestNavigationContainer.tsx index 8ae0725f83bf2..007b8791dbd50 100644 --- a/tests/utils/TestNavigationContainer.tsx +++ b/tests/utils/TestNavigationContainer.tsx @@ -120,10 +120,6 @@ function TestSearchFullscreenNavigator() { name={SCREENS.SEARCH.ROOT} getComponent={getEmptyComponent()} /> - ); }