diff --git a/src/libs/Navigation/AppNavigator/createSplitStackNavigator/SplitStackRouter.ts b/src/libs/Navigation/AppNavigator/createSplitStackNavigator/SplitStackRouter.ts index 7bdb1ba22a3d..bbe741fe25cd 100644 --- a/src/libs/Navigation/AppNavigator/createSplitStackNavigator/SplitStackRouter.ts +++ b/src/libs/Navigation/AppNavigator/createSplitStackNavigator/SplitStackRouter.ts @@ -20,7 +20,7 @@ function adaptStateIfNecessary({state, options: {sidebarScreen, defaultCentralSc const workspaceCentralPane = state.routes.at(-1); // There should always be sidebarScreen screen in the state to make sure go back works properly if we deeplinkg to a subpage of settings. - if (!isAtLeastOneInState(state, sidebarScreen)) { + if (!isAtLeastOneInState(state, sidebarScreen) && !isNarrowLayout) { // @ts-expect-error Updating read only property // noinspection JSConstantReassignment state.stale = true; // eslint-disable-line diff --git a/src/libs/Navigation/AppNavigator/createSplitStackNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createSplitStackNavigator/index.tsx index 80cf27eb1fe6..d90b60d2ac4d 100644 --- a/src/libs/Navigation/AppNavigator/createSplitStackNavigator/index.tsx +++ b/src/libs/Navigation/AppNavigator/createSplitStackNavigator/index.tsx @@ -2,7 +2,7 @@ import type {ParamListBase, StackActionHelpers, StackNavigationState} from '@rea import {createNavigatorFactory, useNavigationBuilder, useRoute} from '@react-navigation/native'; import type {StackNavigationEventMap, StackNavigationOptions} from '@react-navigation/stack'; import {StackView} from '@react-navigation/stack'; -import React, {useMemo} from 'react'; +import React from 'react'; import {View} from 'react-native'; import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -14,25 +14,10 @@ import type {SplitStackNavigatorProps, SplitStackNavigatorRouterOptions} from '. import useHandleScreenResize from './useHandleScreenResize'; import usePrepareSplitStackNavigatorChildren from './usePrepareSplitStackNavigatorChildren'; -function getStateToRender(state: StackNavigationState, isSmallScreenWidth: boolean): StackNavigationState { - const sidebarScreenRoute = state.routes.at(0); - const centralScreenRoutes = state.routes.slice(1); - const routes = isSmallScreenWidth ? state.routes.slice(-2) : [sidebarScreenRoute, ...centralScreenRoutes.slice(-2)]; - - // Routes passed to the state have to be defined - const definedRoutes = routes.filter((route) => route !== undefined); - - return { - ...state, - routes: definedRoutes, - index: routes.length - 1, - }; -} - function SplitStackNavigator(props: SplitStackNavigatorProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const {isSmallScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const screenOptions = getRootNavigatorScreenOptions(shouldUseNarrowLayout, styles, StyleUtils); const children = usePrepareSplitStackNavigatorChildren(props.children, props.sidebarScreen, screenOptions.homeScreen); @@ -58,8 +43,6 @@ function SplitStackNavigator(props: SplitStackN useHandleScreenResize(navigation); - const stateToRender = useMemo(() => getStateToRender(state, isSmallScreenWidth), [state, isSmallScreenWidth]); - return ( @@ -67,7 +50,7 @@ function SplitStackNavigator(props: SplitStackN diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 961aad46c5cf..ed13a2076537 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -1,9 +1,12 @@ -import {findFocusedRoute} from '@react-navigation/core'; -import type {EventArg, NavigationContainerEventMap} from '@react-navigation/native'; +import {getActionFromState} from '@react-navigation/core'; +import type {EventArg, NavigationAction, NavigationContainerEventMap} from '@react-navigation/native'; import {CommonActions, getPathFromState, StackActions} from '@react-navigation/native'; import type {OnyxEntry} from 'react-native-onyx'; +import type {Writable} from 'type-fest'; +import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import Log from '@libs/Log'; -import {isCentralPaneName, removePolicyIDParamFromState} from '@libs/NavigationUtils'; +import {removePolicyIDParamFromState} from '@libs/NavigationUtils'; +import {shallowCompare} from '@libs/ObjectUtils'; import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils'; import * as ReportConnection from '@libs/ReportConnection'; import * as ReportUtils from '@libs/ReportUtils'; @@ -17,17 +20,32 @@ import type {Report} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import originalCloseRHPFlow from './closeRHPFlow'; import getPolicyIDFromState from './getPolicyIDFromState'; -import getTopmostBottomTabRoute from './getTopmostBottomTabRoute'; +import getStateFromPath from './getStateFromPath'; import getTopmostCentralPaneRoute from './getTopmostCentralPaneRoute'; import originalGetTopmostReportActionId from './getTopmostReportActionID'; import originalGetTopmostReportId from './getTopmostReportId'; import isReportOpenInRHP from './isReportOpenInRHP'; import linkingConfig from './linkingConfig'; -import getMatchingBottomTabRouteForState from './linkingConfig/getMatchingBottomTabRouteForState'; +import createSplitNavigator from './linkingConfig/createSplitNavigator'; import linkTo from './linkTo'; +import getMinimalAction from './linkTo/getMinimalAction'; import navigationRef from './navigationRef'; import setNavigationActionToMicrotaskQueue from './setNavigationActionToMicrotaskQueue'; -import type {NavigationStateRoute, RootStackParamList, State, StateOrRoute} from './types'; +import type {NavigationPartialRoute, NavigationStateRoute, RootStackParamList, SplitNavigatorLHNScreen, SplitNavigatorParamListType, State, StateOrRoute} from './types'; + +const SPLIT_NAVIGATOR_TO_SIDEBAR_MAP: Record = { + [NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]: SCREENS.HOME, + [NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR]: SCREENS.SETTINGS.ROOT, + [NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR]: SCREENS.WORKSPACE.INITIAL, +}; + +function getSidebarScreenParams(splitNavigatorRoute: NavigationStateRoute) { + if (splitNavigatorRoute.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR) { + return splitNavigatorRoute.state?.routes?.at(0)?.params; + } + + return undefined; +} let resolveNavigationIsReadyPromise: () => void; const navigationIsReadyPromise = new Promise((resolve) => { @@ -111,8 +129,8 @@ function parseHybridAppUrl(url: HybridAppRoute | Route): Route { * @param path - Path that you are looking for. * @return - Returns distance to path or -1 if the path is not found in root navigator. */ -function getDistanceFromPathInRootNavigator(path?: string): number { - let currentState = navigationRef.getRootState(); +function getDistanceFromPathInRootNavigator(state: State, path?: string): number { + let currentState = {...state}; for (let index = 0; index < 5; index++) { if (!currentState.routes.length) { @@ -188,26 +206,63 @@ function navigate(route: Route = ROUTES.HOME, type?: string) { linkTo(navigationRef.current, route, type, isActiveRoute(route)); } -function newGoBack(fallbackRoute?: Route, shouldEnforceFallback = false, shouldPopToTop = false) { +function doesRouteMatchToMinimalActionPayload(route: NavigationStateRoute | NavigationPartialRoute, minimalAction: Writable) { + if (!minimalAction.payload) { + return false; + } + + if (!('name' in minimalAction.payload)) { + return false; + } + + const areRouteNamesEqual = route.name === minimalAction.payload.name; + + if (!areRouteNamesEqual) { + return false; + } + + if (!('params' in minimalAction.payload)) { + return false; + } + + // @TODO: Fix params comparison. When comparing split navigators params, it may happen that first one has parameters with the initial settings and the second one does not. + return shallowCompare(route.params as Record, minimalAction.payload.params as Record); +} + +function goUp(fallbackRoute: Route) { if (!canNavigate('goBack')) { return; } - if (!navigationRef.current?.canGoBack()) { - Log.hmmm('[Navigation] Unable to go back'); + if (!navigationRef.current) { + Log.hmmm('[Navigation] Unable to go up'); return; } - navigationRef.current.goBack(); - if (fallbackRoute) { - /** - * Cases to handle: - * 1. RHP - * 2. fallbackRoute is in the current navigator - * 3. fallbackRoute is in the different navigator - * 4. fallbackRoute isn't present in the current state - */ + const rootState = navigationRef.current.getRootState(); + const stateFromPath = getStateFromPath(fallbackRoute); + const action = getActionFromState(stateFromPath, linkingConfig.config); + + if (!action) { + return; + } + + const {action: minimalAction, targetState} = getMinimalAction(action, rootState); + + if (minimalAction.type !== CONST.NAVIGATION.ACTION_TYPE.NAVIGATE || !targetState) { + return; + } + + const indexOfFallbackRoute = targetState.routes.findLastIndex((route) => doesRouteMatchToMinimalActionPayload(route, minimalAction)); + + if (indexOfFallbackRoute === -1) { + const replaceAction = {...minimalAction, type: 'REPLACE'} as NavigationAction; + navigationRef.current.dispatch(replaceAction); + return; } + + const distanceToPop = targetState.routes.length - indexOfFallbackRoute - 1; + navigationRef.current.dispatch({...StackActions.pop(distanceToPop), target: targetState.key}); } /** @@ -228,98 +283,52 @@ function goBack(fallbackRoute?: Route, shouldEnforceFallback = false, shouldPopT } } - if (!navigationRef.current?.canGoBack()) { - Log.hmmm('[Navigation] Unable to go back'); - return; - } - - const isFirstRouteInNavigator = !getActiveRouteIndex(navigationRef.current.getState()); - if (isFirstRouteInNavigator) { - const rootState = navigationRef.getRootState(); - const lastRoute = rootState.routes.at(-1); - // If the user comes from a different flow (there is more than one route in ModalNavigator) we should go back to the previous flow on UP button press instead of using the fallbackRoute. - if ((lastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR || lastRoute?.name === NAVIGATORS.LEFT_MODAL_NAVIGATOR) && (lastRoute.state?.index ?? 0) > 0) { - navigationRef.current.goBack(); - return; - } - } - - if (shouldEnforceFallback || (isFirstRouteInNavigator && fallbackRoute)) { - navigate(fallbackRoute, 'REPLACE'); + if (fallbackRoute) { + goUp(fallbackRoute); return; } - const isCentralPaneFocused = isCentralPaneName(findFocusedRoute(navigationRef.current.getState())?.name); - const distanceFromPathInRootNavigator = getDistanceFromPathInRootNavigator(fallbackRoute ?? ''); - - if (isCentralPaneFocused && fallbackRoute) { - // Allow CentralPane to use UP with fallback route if the path is not found in root navigator. - if (distanceFromPathInRootNavigator === -1) { - navigate(fallbackRoute, 'REPLACE'); - return; - } + const rootState = navigationRef.current?.getRootState(); + const lastRoute = rootState?.routes.at(-1); - // Add possibility to go back more than one screen in root navigator if that screen is on the stack. - if (distanceFromPathInRootNavigator > 0) { - navigationRef.current.dispatch(StackActions.pop(distanceFromPathInRootNavigator)); - return; - } - } + const canGoBack = navigationRef.current?.canGoBack(); - // If the central pane is focused, it's possible that we navigated from other central pane with different matching bottom tab. - if (isCentralPaneFocused) { - const rootState = navigationRef.getRootState(); - const stateAfterPop = {routes: rootState.routes.slice(0, -1)} as State; - const topmostCentralPaneRouteAfterPop = getTopmostCentralPaneRoute(stateAfterPop); - - const topmostBottomTabRoute = getTopmostBottomTabRoute(rootState as State); - const matchingBottomTabRoute = getMatchingBottomTabRouteForState(stateAfterPop); - - // If the central pane is defined after the pop action, we need to check if it's synced with the bottom tab screen. - // If not, we need to pop to the bottom tab screen/screens to sync it with the new central pane. - if (topmostCentralPaneRouteAfterPop && topmostBottomTabRoute?.name !== matchingBottomTabRoute.name) { - const bottomTabNavigator = rootState.routes.find((item: NavigationStateRoute) => item.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR)?.state; - - if (bottomTabNavigator && bottomTabNavigator.index) { - const matchingIndex = bottomTabNavigator.routes.findLastIndex((item) => item.name === matchingBottomTabRoute.name); - const indexToPop = matchingIndex !== -1 ? bottomTabNavigator.index - matchingIndex : undefined; - navigationRef.current.dispatch({...StackActions.pop(indexToPop), target: bottomTabNavigator?.key}); - } - } - } - - navigationRef.current.goBack(); -} - -/** - * Close the current screen and navigate to the route. - * If the current screen is the first screen in the navigator, we force using the fallback route to replace the current screen. - * It's useful in a case where we want to close an RHP and navigate to another RHP to prevent any blink effect. - */ -function closeAndNavigate(route: Route) { - if (!navigationRef.current) { + if (!canGoBack && lastRoute?.name.endsWith('SplitNavigator') && lastRoute?.state?.routes?.length === 1) { + const splitNavigatorName = lastRoute?.name as keyof SplitNavigatorParamListType; + const name = SPLIT_NAVIGATOR_TO_SIDEBAR_MAP[splitNavigatorName]; + const params = getSidebarScreenParams(lastRoute); + navigationRef.dispatch({ + type: 'REPLACE', + payload: { + name, + params, + }, + }); return; } - const isFirstRouteInNavigator = !getActiveRouteIndex(navigationRef.current.getState()); - if (isFirstRouteInNavigator) { - goBack(route, true); + if (!canGoBack) { + Log.hmmm('[Navigation] Unable to go back'); return; } - goBack(); - navigate(route); + + navigationRef.current?.goBack(); } /** * Reset the navigation state to Home page */ function resetToHome() { + const isNarrowLayout = getIsNarrowLayout(); const rootState = navigationRef.getRootState(); - const bottomTabKey = rootState.routes.find((item: NavigationStateRoute) => item.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR)?.state?.key; - if (bottomTabKey) { - navigationRef.dispatch({...StackActions.popToTop(), target: bottomTabKey}); - } navigationRef.dispatch({...StackActions.popToTop(), target: rootState.key}); + const splitNavigatorMainScreen = !isNarrowLayout + ? { + name: SCREENS.REPORT, + } + : undefined; + const payload = createSplitNavigator({name: SCREENS.HOME}, splitNavigatorMainScreen); + navigationRef.dispatch({payload, type: 'REPLACE', target: rootState.key}); } /** @@ -487,7 +496,6 @@ export default { getActiveRoute, getActiveRouteWithoutParams, getReportRHPActiveRoute, - closeAndNavigate, goBack, isNavigationReady, setIsNavigationReady, @@ -502,6 +510,7 @@ export default { setNavigationActionToMicrotaskQueue, getTopMostCentralPaneRouteFromRootState, navigateToReportWithPolicyCheck, + goUp, }; export {navigationRef}; diff --git a/src/libs/Navigation/linkTo/getMinimalAction.ts b/src/libs/Navigation/linkTo/getMinimalAction.ts index ff01b3b8333b..9eab2f6f8717 100644 --- a/src/libs/Navigation/linkTo/getMinimalAction.ts +++ b/src/libs/Navigation/linkTo/getMinimalAction.ts @@ -3,6 +3,11 @@ import type {Writable} from 'type-fest'; import type {State} from '@navigation/types'; import type {ActionPayload} from './types'; +type MinimalAction = { + action: Writable; + targetState: State | undefined; +}; + /** * Motivation for this function is described in NAVIGATION.md * @@ -10,7 +15,7 @@ import type {ActionPayload} from './types'; * @param state The root state * @returns minimalAction minimal action is the action that we should dispatch */ -function getMinimalAction(action: NavigationAction, state: NavigationState): Writable { +function getMinimalAction(action: NavigationAction, state: NavigationState): MinimalAction { let currentAction: NavigationAction = action; let currentState: State | undefined = state; let currentTargetKey: string | undefined; @@ -36,7 +41,7 @@ function getMinimalAction(action: NavigationAction, state: NavigationState): Wri target: currentTargetKey, }; } - return currentAction; + return {action: currentAction, targetState: currentState}; } export default getMinimalAction; diff --git a/src/libs/Navigation/linkTo/index.ts b/src/libs/Navigation/linkTo/index.ts index f8ac74d6e61f..40d51cd6ec9f 100644 --- a/src/libs/Navigation/linkTo/index.ts +++ b/src/libs/Navigation/linkTo/index.ts @@ -56,6 +56,6 @@ export default function linkTo(navigation: NavigationContainerRef { User.requestRefund(); setIsRequestRefundModalVisible(false); - Navigation.resetToHome(); + Navigation.goUp(ROUTES.HOME); }, []); const viewPurchases = useCallback(() => { diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx index 5deae769531d..be748a50bb5d 100644 --- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -91,7 +91,7 @@ function PageNotFoundFallback({policyID, shouldShowFullScreenFallback, fullPageN shouldForceFullScreen={shouldShowFullScreenFallback} onBackButtonPress={() => { if (shouldShowFullScreenFallback) { - Navigation.dismissModal(); + Navigation.goUp(ROUTES.SETTINGS_WORKSPACES); return; } Navigation.goBack(policyID && !isMoneyRequest ? ROUTES.WORKSPACE_PROFILE.getRoute(policyID) : undefined); diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 1644fdac1abc..fed578c4c1f9 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -24,7 +24,7 @@ import {isConnectionInProgress} from '@libs/actions/connections'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import BottomTabBar from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar'; import getTopmostRouteName from '@libs/Navigation/getTopmostRouteName'; -import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; +import Navigation from '@libs/Navigation/Navigation'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; @@ -368,7 +368,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac > Navigation.goUp(ROUTES.HOME)} shouldShow={shouldShowNotFoundPage} subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} > @@ -376,19 +376,10 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac title={policyName} onBackButtonPress={() => { if (route.params?.backTo) { - Navigation.resetToHome(); + Navigation.goUp(ROUTES.HOME); Navigation.isNavigationReady().then(() => Navigation.navigate(route.params?.backTo as Route)); } else { - // @TODO This part could be done with the new goBack method when it will be implemented. - const previousRoute = navigationRef.getRootState().routes.at(-2); - - // If there is the settings split navigator we can dismiss safely - if (previousRoute?.name === NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR) { - Navigation.dismissModal(); - } else { - // If not, we are going to replace this route with the settings route - Navigation.navigate(ROUTES.SETTINGS_WORKSPACES, CONST.NAVIGATION.ACTION_TYPE.REPLACE); - } + Navigation.goUp(ROUTES.SETTINGS_WORKSPACES); } }} policyAvatar={policyAvatar} diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 4f0a84cffd9c..d7999971cca7 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -21,6 +21,7 @@ import * as BankAccounts from '@userActions/BankAccounts'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; +import ROUTES from '@src/ROUTES'; import type {Policy} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -173,8 +174,8 @@ function WorkspacePageWithSections({ shouldShowOfflineIndicatorInWideScreen={shouldShowOfflineIndicatorInWideScreen && !shouldShow} > Navigation.goUp(ROUTES.SETTINGS_WORKSPACES)} + onLinkPress={() => Navigation.goUp(ROUTES.HOME)} shouldShow={shouldShow} subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} shouldForceFullScreen diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 95d1459062d0..a29e17ef61d2 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -108,7 +108,7 @@ function WorkspacesListPage() { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const {shouldUseNarrowLayout, isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout(); + const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); const [allConnectionSyncProgresses] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);