diff --git a/src/NAVIGATORS.ts b/src/NAVIGATORS.ts
index a3a041e65684..c68a950d3501 100644
--- a/src/NAVIGATORS.ts
+++ b/src/NAVIGATORS.ts
@@ -4,6 +4,7 @@
* */
export default {
CENTRAL_PANE_NAVIGATOR: 'CentralPaneNavigator',
+ LEFT_MODAL_NAVIGATOR: 'LeftModalNavigator',
RIGHT_MODAL_NAVIGATOR: 'RightModalNavigator',
FULL_SCREEN_NAVIGATOR: 'FullScreenNavigator',
} as const;
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index c1d2059cd3b0..c5107f1c9c29 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -81,10 +81,12 @@ const SCREENS = {
SAVE_THE_WORLD: {
ROOT: 'SaveTheWorld_Root',
},
+ LEFT_MODAL: {
+ SEARCH: 'Search',
+ },
RIGHT_MODAL: {
SETTINGS: 'Settings',
NEW_CHAT: 'NewChat',
- SEARCH: 'Search',
DETAILS: 'Details',
PROFILE: 'Profile',
REPORT_DETAILS: 'Report_Details',
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index b6304cb3b1b7..fdef49d71eae 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -35,6 +35,7 @@ import createCustomStackNavigator from './createCustomStackNavigator';
import defaultScreenOptions from './defaultScreenOptions';
import getRootNavigatorScreenOptions from './getRootNavigatorScreenOptions';
import CentralPaneNavigator from './Navigators/CentralPaneNavigator';
+import LeftModalNavigator from './Navigators/LeftModalNavigator';
import RightModalNavigator from './Navigators/RightModalNavigator';
type AuthScreensProps = {
@@ -295,6 +296,12 @@ function AuthScreens({lastUpdateIDAppliedToClient, session, lastOpenedPublicRoom
component={RightModalNavigator}
listeners={modalScreenListeners}
/>
+
({
+const ModalNavigatorScreenOptions = (themeStyles: ThemeStyles): StackNavigationOptions => ({
headerShown: false,
animationEnabled: true,
gestureDirection: 'horizontal',
@@ -14,4 +14,4 @@ const RHPScreenOptions = (themeStyles: ThemeStyles): StackNavigationOptions => (
cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS,
});
-export default RHPScreenOptions;
+export default ModalNavigatorScreenOptions;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx
new file mode 100644
index 000000000000..b7385c930e2c
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx
@@ -0,0 +1,45 @@
+import {createStackNavigator, StackScreenProps} from '@react-navigation/stack';
+import React, {useMemo} from 'react';
+import {View} from 'react-native';
+import NoDropZone from '@components/DragAndDrop/NoDropZone';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import ModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/ModalNavigatorScreenOptions';
+import * as ModalStackNavigators from '@libs/Navigation/AppNavigator/ModalStackNavigators';
+import {AuthScreensParamList, LeftModalNavigatorParamList} from '@libs/Navigation/types';
+import NAVIGATORS from '@src/NAVIGATORS';
+import SCREENS from '@src/SCREENS';
+import Overlay from './Overlay';
+
+type LeftModalNavigatorProps = StackScreenProps;
+
+const Stack = createStackNavigator();
+
+function LeftModalNavigator({navigation}: LeftModalNavigatorProps) {
+ const styles = useThemeStyles();
+ const {isSmallScreenWidth} = useWindowDimensions();
+ const screenOptions = useMemo(() => ModalNavigatorScreenOptions(styles), [styles]);
+
+ return (
+
+ {!isSmallScreenWidth && (
+
+ )}
+
+
+
+
+
+
+ );
+}
+
+LeftModalNavigator.displayName = 'LeftModalNavigator';
+
+export default LeftModalNavigator;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx b/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
index 065de8da578b..a3fe1c657f34 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
@@ -9,15 +9,18 @@ import CONST from '@src/CONST';
type OverlayProps = {
/* Callback to close the modal */
onPress: () => void;
+
+ /* Returns whether a modal is displayed on the left side of the screen. By default, the modal is displayed on the right */
+ isModalOnTheLeft?: boolean;
};
-function Overlay({onPress}: OverlayProps) {
+function Overlay({onPress, isModalOnTheLeft = false}: OverlayProps) {
const styles = useThemeStyles();
const {current} = useCardAnimation();
const {translate} = useLocalize();
return (
-
+
{/* In the latest Electron version buttons can't be both clickable and draggable.
That's why we added this workaround. Because of two Pressable components on the desktop app
diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
index bd790589c8d1..d7c31bcae7d9 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
@@ -4,8 +4,8 @@ import {View} from 'react-native';
import NoDropZone from '@components/DragAndDrop/NoDropZone';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import ModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/ModalNavigatorScreenOptions';
import * as ModalStackNavigators from '@libs/Navigation/AppNavigator/ModalStackNavigators';
-import RHPScreenOptions from '@libs/Navigation/AppNavigator/RHPScreenOptions';
import type {AuthScreensParamList, RightModalNavigatorParamList} from '@navigation/types';
import NAVIGATORS from '@src/NAVIGATORS';
import SCREENS from '@src/SCREENS';
@@ -18,7 +18,7 @@ const Stack = createStackNavigator();
function RightModalNavigator({navigation}: RightModalNavigatorProps) {
const styles = useThemeStyles();
const {isSmallScreenWidth} = useWindowDimensions();
- const screenOptions = useMemo(() => RHPScreenOptions(styles), [styles]);
+ const screenOptions = useMemo(() => ModalNavigatorScreenOptions(styles), [styles]);
return (
@@ -33,10 +33,6 @@ function RightModalNavigator({navigation}: RightModalNavigatorProps) {
name={SCREENS.RIGHT_MODAL.NEW_CHAT}
component={ModalStackNavigators.NewChatModalStackNavigator}
/>
-
({
rightModalNavigator: {
...commonScreenOptions,
@@ -32,7 +34,23 @@ export default (isSmallScreenWidth: boolean, themeStyles: ThemeStyles): ScreenOp
right: 0,
},
},
+ leftModalNavigator: {
+ ...commonScreenOptions,
+ cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, false, props, SLIDE_LEFT_OUTPUT_RANGE_MULTIPLIER),
+ presentation: 'transparentModal',
+
+ // We want pop in LHP since there are some flows that would work weird otherwise
+ animationTypeForReplace: 'pop',
+ cardStyle: {
+ ...getNavigationModalCardStyle(),
+
+ // This is necessary to cover translated sidebar with overlay.
+ width: isSmallScreenWidth ? '100%' : '200%',
+ // LHP should be displayed in place of the sidebar
+ left: isSmallScreenWidth ? 0 : -variables.sideBarWidth,
+ },
+ },
homeScreen: {
title: CONFIG.SITE_TITLE,
...commonScreenOptions,
diff --git a/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.ts b/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.ts
index eff88422cc5c..fd59b02e724d 100644
--- a/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.ts
+++ b/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.ts
@@ -3,11 +3,16 @@ import {Animated} from 'react-native';
import getCardStyles from '@styles/utils/cardStyles';
import variables from '@styles/variables';
-export default (isSmallScreenWidth: boolean, isFullScreenModal: boolean, {current: {progress}, inverted, layouts: {screen}}: StackCardInterpolationProps): StackCardInterpolatedStyle => {
+export default (
+ isSmallScreenWidth: boolean,
+ isFullScreenModal: boolean,
+ {current: {progress}, inverted, layouts: {screen}}: StackCardInterpolationProps,
+ outputRangeMultiplier = 1,
+): StackCardInterpolatedStyle => {
const translateX = Animated.multiply(
progress.interpolate({
inputRange: [0, 1],
- outputRange: [isSmallScreenWidth ? screen.width : variables.sideBarWidth, 0],
+ outputRange: [outputRangeMultiplier * (isSmallScreenWidth ? screen.width : variables.sideBarWidth), 0],
extrapolate: 'clamp',
}),
inverted,
diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts
index 3552ff9e7410..a3e89a983f98 100644
--- a/src/libs/Navigation/Navigation.ts
+++ b/src/libs/Navigation/Navigation.ts
@@ -1,18 +1,17 @@
-import {findFocusedRoute, getActionFromState} from '@react-navigation/core';
+import {findFocusedRoute} from '@react-navigation/core';
import {CommonActions, EventArg, getPathFromState, NavigationContainerEventMap, NavigationState, PartialState, StackActions} from '@react-navigation/native';
-import findLastIndex from 'lodash/findLastIndex';
import Log from '@libs/Log';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import ROUTES, {Route} from '@src/ROUTES';
-import SCREENS, {PROTECTED_SCREENS} from '@src/SCREENS';
-import getStateFromPath from './getStateFromPath';
+import {PROTECTED_SCREENS} from '@src/SCREENS';
+import originalDismissModal from './dismissModal';
import originalGetTopmostReportActionId from './getTopmostReportActionID';
import originalGetTopmostReportId from './getTopmostReportId';
import linkingConfig from './linkingConfig';
import linkTo from './linkTo';
import navigationRef from './navigationRef';
-import {StackNavigationAction, StateOrRoute} from './types';
+import {StateOrRoute} from './types';
let resolveNavigationIsReadyPromise: () => void;
const navigationIsReadyPromise = new Promise((resolve) => {
@@ -44,6 +43,9 @@ const getTopmostReportId = (state = navigationRef.getState()) => originalGetTopm
// Re-exporting the getTopmostReportActionID here to fill in default value for state. The getTopmostReportActionID isn't defined in this file to avoid cyclic dependencies.
const getTopmostReportActionId = (state = navigationRef.getState()) => originalGetTopmostReportActionId(state);
+// Re-exporting the dismissModal here to fill in default value for navigationRef. The dismissModal isn't defined in this file to avoid cyclic dependencies.
+const dismissModal = (targetReportId = '', ref = navigationRef) => originalDismissModal(targetReportId, ref);
+
/** Method for finding on which index in stack we are. */
function getActiveRouteIndex(stateOrRoute: StateOrRoute, index?: number): number | undefined {
if ('routes' in stateOrRoute && stateOrRoute.routes) {
@@ -56,7 +58,7 @@ function getActiveRouteIndex(stateOrRoute: StateOrRoute, index?: number): number
return getActiveRouteIndex(childActiveRoute, stateOrRoute.state.index ?? 0);
}
- if ('name' in stateOrRoute && stateOrRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
+ if ('name' in stateOrRoute && (stateOrRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR || stateOrRoute.name === NAVIGATORS.LEFT_MODAL_NAVIGATOR)) {
return 0;
}
@@ -160,8 +162,8 @@ function goBack(fallbackRoute: Route, shouldEnforceFallback = false, shouldPopTo
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 RHP) 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.state?.index ?? 0) > 0) {
+ // 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;
}
@@ -200,45 +202,6 @@ function setParams(params: Record, routeKey: string) {
});
}
-/**
- * Dismisses the last modal stack if there is any
- *
- * @param targetReportID - The reportID to navigate to after dismissing the modal
- */
-function dismissModal(targetReportID?: string) {
- if (!canNavigate('dismissModal')) {
- return;
- }
- const rootState = navigationRef.getRootState();
- const lastRoute = rootState.routes.at(-1);
- switch (lastRoute?.name) {
- case NAVIGATORS.RIGHT_MODAL_NAVIGATOR:
- case SCREENS.NOT_FOUND:
- case SCREENS.REPORT_ATTACHMENTS:
- // if we are not in the target report, we need to navigate to it after dismissing the modal
- if (targetReportID && targetReportID !== getTopmostReportId(rootState)) {
- const state = getStateFromPath(ROUTES.REPORT_WITH_ID.getRoute(targetReportID));
-
- const action: StackNavigationAction = getActionFromState(state, linkingConfig.config);
- if (action) {
- action.type = 'REPLACE';
- navigationRef.current?.dispatch(action);
- }
- // If not-found page is in the route stack, we need to close it
- } else if (targetReportID && rootState.routes.some((route) => route.name === SCREENS.NOT_FOUND)) {
- const lastRouteIndex = rootState.routes.length - 1;
- const centralRouteIndex = findLastIndex(rootState.routes, (route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR);
- navigationRef.current?.dispatch({...StackActions.pop(lastRouteIndex - centralRouteIndex), target: rootState.key});
- } else {
- navigationRef.current?.dispatch({...StackActions.pop(), target: rootState.key});
- }
- break;
- default: {
- Log.hmmm('[Navigation] dismissModal failed because there is no modal stack to dismiss');
- }
- }
-}
-
/**
* Returns the current active route without the URL params
*/
diff --git a/src/libs/Navigation/dismissModal.ts b/src/libs/Navigation/dismissModal.ts
new file mode 100644
index 000000000000..37b4c6d9b9e6
--- /dev/null
+++ b/src/libs/Navigation/dismissModal.ts
@@ -0,0 +1,56 @@
+import {getActionFromState} from '@react-navigation/core';
+import {NavigationContainerRef, StackActions} from '@react-navigation/native';
+import {findLastIndex} from 'lodash';
+import Log from '@libs/Log';
+import NAVIGATORS from '@src/NAVIGATORS';
+import ROUTES from '@src/ROUTES';
+import SCREENS from '@src/SCREENS';
+import getStateFromPath from './getStateFromPath';
+import getTopmostReportId from './getTopmostReportId';
+import linkingConfig from './linkingConfig';
+import {RootStackParamList, StackNavigationAction} from './types';
+
+// This function is in a separate file than Navigation.js to avoid cyclic dependency.
+
+/**
+ * Dismisses the last modal stack if there is any
+ *
+ * @param targetReportID - The reportID to navigate to after dismissing the modal
+ */
+function dismissModal(targetReportID: string, navigationRef: NavigationContainerRef) {
+ if (!navigationRef.isReady()) {
+ return;
+ }
+
+ const state = navigationRef.getState();
+ const lastRoute = state.routes.at(-1);
+ switch (lastRoute?.name) {
+ case NAVIGATORS.LEFT_MODAL_NAVIGATOR:
+ case NAVIGATORS.RIGHT_MODAL_NAVIGATOR:
+ case SCREENS.NOT_FOUND:
+ case SCREENS.REPORT_ATTACHMENTS:
+ // if we are not in the target report, we need to navigate to it after dismissing the modal
+ if (targetReportID && targetReportID !== getTopmostReportId(state)) {
+ const reportState = getStateFromPath(ROUTES.REPORT_WITH_ID.getRoute(targetReportID));
+
+ const action: StackNavigationAction = getActionFromState(reportState, linkingConfig.config);
+ if (action) {
+ action.type = 'REPLACE';
+ navigationRef.dispatch(action);
+ }
+ // If not-found page is in the route stack, we need to close it
+ } else if (targetReportID && state.routes.some((route) => route.name === SCREENS.NOT_FOUND)) {
+ const lastRouteIndex = state.routes.length - 1;
+ const centralRouteIndex = findLastIndex(state.routes, (route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR);
+ navigationRef.dispatch({...StackActions.pop(lastRouteIndex - centralRouteIndex), target: state.key});
+ } else {
+ navigationRef.dispatch({...StackActions.pop(), target: state.key});
+ }
+ break;
+ default: {
+ Log.hmmm('[Navigation] dismissModal failed because there is no modal stack to dismiss');
+ }
+ }
+}
+
+export default dismissModal;
diff --git a/src/libs/Navigation/linkTo.ts b/src/libs/Navigation/linkTo.ts
index 9694879f9aae..86558765a6e6 100644
--- a/src/libs/Navigation/linkTo.ts
+++ b/src/libs/Navigation/linkTo.ts
@@ -4,6 +4,7 @@ import {Writable} from 'type-fest';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import {Route} from '@src/ROUTES';
+import dismissModal from './dismissModal';
import getStateFromPath from './getStateFromPath';
import getTopmostReportId from './getTopmostReportId';
import linkingConfig from './linkingConfig';
@@ -55,6 +56,10 @@ function getMinimalAction(action: NavigationAction, state: NavigationState): Wri
return currentAction;
}
+function isModalNavigator(targetNavigator?: string) {
+ return targetNavigator === NAVIGATORS.LEFT_MODAL_NAVIGATOR || targetNavigator === NAVIGATORS.RIGHT_MODAL_NAVIGATOR;
+}
+
export default function linkTo(navigation: NavigationContainerRef | null, path: Route, type?: string, isActiveRoute?: boolean) {
if (!navigation) {
throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?");
@@ -75,6 +80,9 @@ export default function linkTo(navigation: NavigationContainerRef = {
},
},
[SCREENS.NOT_FOUND]: '*',
-
+ [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: {
+ screens: {
+ [SCREENS.LEFT_MODAL.SEARCH]: {
+ screens: {
+ [SCREENS.SEARCH_ROOT]: ROUTES.SEARCH,
+ },
+ },
+ },
+ },
[NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: {
screens: {
[SCREENS.RIGHT_MODAL.SETTINGS]: {
@@ -338,11 +346,6 @@ const linkingConfig: LinkingOptions = {
[SCREENS.I_AM_A_TEACHER]: ROUTES.I_AM_A_TEACHER,
},
},
- [SCREENS.RIGHT_MODAL.SEARCH]: {
- screens: {
- [SCREENS.SEARCH_ROOT]: ROUTES.SEARCH,
- },
- },
[SCREENS.RIGHT_MODAL.DETAILS]: {
screens: {
[SCREENS.DETAILS_ROOT]: ROUTES.DETAILS.route,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 1217e2cfa6b1..e55fcc388870 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -334,10 +334,13 @@ type PrivateNotesNavigatorParamList = {
};
};
+type LeftModalNavigatorParamList = {
+ [SCREENS.LEFT_MODAL.SEARCH]: NavigatorScreenParams;
+};
+
type RightModalNavigatorParamList = {
[SCREENS.RIGHT_MODAL.SETTINGS]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.NEW_CHAT]: NavigatorScreenParams;
- [SCREENS.RIGHT_MODAL.SEARCH]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.DETAILS]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.PROFILE]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.REPORT_DETAILS]: NavigatorScreenParams;
@@ -401,6 +404,7 @@ type AuthScreensParamList = {
source: string;
};
[SCREENS.NOT_FOUND]: undefined;
+ [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: NavigatorScreenParams;
[NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: NavigatorScreenParams;
[SCREENS.DESKTOP_SIGN_IN_REDIRECT]: undefined;
};
@@ -416,6 +420,7 @@ export type {
NavigationStateRoute,
NavigationRoot,
AuthScreensParamList,
+ LeftModalNavigatorParamList,
RightModalNavigatorParamList,
PublicScreensParamList,
MoneyRequestNavigatorParamList,
diff --git a/src/styles/index.ts b/src/styles/index.ts
index da3c2bc2608c..fb1919b9f5d3 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -1422,6 +1422,12 @@ const styles = (theme: ThemeColors) =>
height: variables.lineHeightSizeh1,
},
+ LHPNavigatorContainer: (isSmallScreenWidth: boolean) =>
+ ({
+ ...modalNavigatorContainer(isSmallScreenWidth),
+ left: 0,
+ } satisfies ViewStyle),
+
RHPNavigatorContainer: (isSmallScreenWidth: boolean) =>
({
...modalNavigatorContainer(isSmallScreenWidth),
@@ -1641,14 +1647,14 @@ const styles = (theme: ThemeColors) =>
marginBottom: 4,
},
- overlayStyles: (current: OverlayStylesParams) =>
+ overlayStyles: (current: OverlayStylesParams, isModalOnTheLeft: boolean) =>
({
...positioning.pFixed,
// We need to stretch the overlay to cover the sidebar and the translate animation distance.
- left: -2 * variables.sideBarWidth,
+ left: isModalOnTheLeft ? 0 : -2 * variables.sideBarWidth,
top: 0,
bottom: 0,
- right: 0,
+ right: isModalOnTheLeft ? -2 * variables.sideBarWidth : 0,
backgroundColor: theme.overlay,
opacity: current.progress.interpolate({
inputRange: [0, 1],