Skip to content

Commit 648c000

Browse files
authored
Merge pull request #32473 from software-mansion-labs/ideal-nav-lhp
Ideal nav LHP
2 parents f017c6b + b54996f commit 648c000

16 files changed

+198
-77
lines changed

src/NAVIGATORS.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* */
55
export default {
66
CENTRAL_PANE_NAVIGATOR: 'CentralPaneNavigator',
7+
LEFT_MODAL_NAVIGATOR: 'LeftModalNavigator',
78
RIGHT_MODAL_NAVIGATOR: 'RightModalNavigator',
89
FULL_SCREEN_NAVIGATOR: 'FullScreenNavigator',
910
} as const;

src/SCREENS.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,12 @@ const SCREENS = {
8181
SAVE_THE_WORLD: {
8282
ROOT: 'SaveTheWorld_Root',
8383
},
84+
LEFT_MODAL: {
85+
SEARCH: 'Search',
86+
},
8487
RIGHT_MODAL: {
8588
SETTINGS: 'Settings',
8689
NEW_CHAT: 'NewChat',
87-
SEARCH: 'Search',
8890
DETAILS: 'Details',
8991
PROFILE: 'Profile',
9092
REPORT_DETAILS: 'Report_Details',

src/libs/Navigation/AppNavigator/AuthScreens.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import createCustomStackNavigator from './createCustomStackNavigator';
3636
import defaultScreenOptions from './defaultScreenOptions';
3737
import getRootNavigatorScreenOptions from './getRootNavigatorScreenOptions';
3838
import CentralPaneNavigator from './Navigators/CentralPaneNavigator';
39+
import LeftModalNavigator from './Navigators/LeftModalNavigator';
3940
import RightModalNavigator from './Navigators/RightModalNavigator';
4041

4142
type AuthScreensProps = {
@@ -318,6 +319,12 @@ function AuthScreens({lastUpdateIDAppliedToClient, session, lastOpenedPublicRoom
318319
component={RightModalNavigator}
319320
listeners={modalScreenListeners}
320321
/>
322+
<RootStack.Screen
323+
name={NAVIGATORS.LEFT_MODAL_NAVIGATOR}
324+
options={screenOptions.leftModalNavigator}
325+
component={LeftModalNavigator}
326+
listeners={modalScreenListeners}
327+
/>
321328
<RootStack.Screen
322329
name={SCREENS.DESKTOP_SIGN_IN_REDIRECT}
323330
options={screenOptions.fullScreen}

src/libs/Navigation/AppNavigator/RHPScreenOptions.ts renamed to src/libs/Navigation/AppNavigator/ModalNavigatorScreenOptions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import {CardStyleInterpolators, StackNavigationOptions} from '@react-navigation/
22
import {ThemeStyles} from '@styles/index';
33

44
/**
5-
* RHP stack navigator screen options generator function
5+
* Modal stack navigator screen options generator function
66
* @param themeStyles - The styles object
77
* @returns The screen options object
88
*/
9-
const RHPScreenOptions = (themeStyles: ThemeStyles): StackNavigationOptions => ({
9+
const ModalNavigatorScreenOptions = (themeStyles: ThemeStyles): StackNavigationOptions => ({
1010
headerShown: false,
1111
animationEnabled: true,
1212
gestureDirection: 'horizontal',
1313
cardStyle: themeStyles.navigationScreenCardStyle,
1414
cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS,
1515
});
1616

17-
export default RHPScreenOptions;
17+
export default ModalNavigatorScreenOptions;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {createStackNavigator, StackScreenProps} from '@react-navigation/stack';
2+
import React, {useMemo} from 'react';
3+
import {View} from 'react-native';
4+
import NoDropZone from '@components/DragAndDrop/NoDropZone';
5+
import useThemeStyles from '@hooks/useThemeStyles';
6+
import useWindowDimensions from '@hooks/useWindowDimensions';
7+
import ModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/ModalNavigatorScreenOptions';
8+
import * as ModalStackNavigators from '@libs/Navigation/AppNavigator/ModalStackNavigators';
9+
import {AuthScreensParamList, LeftModalNavigatorParamList} from '@libs/Navigation/types';
10+
import NAVIGATORS from '@src/NAVIGATORS';
11+
import SCREENS from '@src/SCREENS';
12+
import Overlay from './Overlay';
13+
14+
type LeftModalNavigatorProps = StackScreenProps<AuthScreensParamList, typeof NAVIGATORS.LEFT_MODAL_NAVIGATOR>;
15+
16+
const Stack = createStackNavigator<LeftModalNavigatorParamList>();
17+
18+
function LeftModalNavigator({navigation}: LeftModalNavigatorProps) {
19+
const styles = useThemeStyles();
20+
const {isSmallScreenWidth} = useWindowDimensions();
21+
const screenOptions = useMemo(() => ModalNavigatorScreenOptions(styles), [styles]);
22+
23+
return (
24+
<NoDropZone>
25+
{!isSmallScreenWidth && (
26+
<Overlay
27+
isModalOnTheLeft
28+
onPress={navigation.goBack}
29+
/>
30+
)}
31+
<View style={styles.LHPNavigatorContainer(isSmallScreenWidth)}>
32+
<Stack.Navigator screenOptions={screenOptions}>
33+
<Stack.Screen
34+
name={SCREENS.LEFT_MODAL.SEARCH}
35+
component={ModalStackNavigators.SearchModalStackNavigator}
36+
/>
37+
</Stack.Navigator>
38+
</View>
39+
</NoDropZone>
40+
);
41+
}
42+
43+
LeftModalNavigator.displayName = 'LeftModalNavigator';
44+
45+
export default LeftModalNavigator;

src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,18 @@ import CONST from '@src/CONST';
99
type OverlayProps = {
1010
/* Callback to close the modal */
1111
onPress: () => void;
12+
13+
/* Returns whether a modal is displayed on the left side of the screen. By default, the modal is displayed on the right */
14+
isModalOnTheLeft?: boolean;
1215
};
1316

14-
function Overlay({onPress}: OverlayProps) {
17+
function Overlay({onPress, isModalOnTheLeft = false}: OverlayProps) {
1518
const styles = useThemeStyles();
1619
const {current} = useCardAnimation();
1720
const {translate} = useLocalize();
1821

1922
return (
20-
<Animated.View style={styles.overlayStyles(current)}>
23+
<Animated.View style={styles.overlayStyles(current, isModalOnTheLeft)}>
2124
<View style={[styles.flex1, styles.flexColumn]}>
2225
{/* In the latest Electron version buttons can't be both clickable and draggable.
2326
That's why we added this workaround. Because of two Pressable components on the desktop app

src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import {View} from 'react-native';
44
import NoDropZone from '@components/DragAndDrop/NoDropZone';
55
import useThemeStyles from '@hooks/useThemeStyles';
66
import useWindowDimensions from '@hooks/useWindowDimensions';
7+
import ModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/ModalNavigatorScreenOptions';
78
import * as ModalStackNavigators from '@libs/Navigation/AppNavigator/ModalStackNavigators';
8-
import RHPScreenOptions from '@libs/Navigation/AppNavigator/RHPScreenOptions';
99
import type {AuthScreensParamList, RightModalNavigatorParamList} from '@navigation/types';
1010
import NAVIGATORS from '@src/NAVIGATORS';
1111
import SCREENS from '@src/SCREENS';
@@ -18,7 +18,7 @@ const Stack = createStackNavigator<RightModalNavigatorParamList>();
1818
function RightModalNavigator({navigation}: RightModalNavigatorProps) {
1919
const styles = useThemeStyles();
2020
const {isSmallScreenWidth} = useWindowDimensions();
21-
const screenOptions = useMemo(() => RHPScreenOptions(styles), [styles]);
21+
const screenOptions = useMemo(() => ModalNavigatorScreenOptions(styles), [styles]);
2222

2323
return (
2424
<NoDropZone>
@@ -33,10 +33,6 @@ function RightModalNavigator({navigation}: RightModalNavigatorProps) {
3333
name={SCREENS.RIGHT_MODAL.NEW_CHAT}
3434
component={ModalStackNavigators.NewChatModalStackNavigator}
3535
/>
36-
<Stack.Screen
37-
name={SCREENS.RIGHT_MODAL.SEARCH}
38-
component={ModalStackNavigators.SearchModalStackNavigator}
39-
/>
4036
<Stack.Screen
4137
name={SCREENS.RIGHT_MODAL.DETAILS}
4238
component={ModalStackNavigators.DetailsModalStackNavigator}

src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const commonScreenOptions: StackNavigationOptions = {
1515
animationTypeForReplace: 'push',
1616
};
1717

18+
const SLIDE_LEFT_OUTPUT_RANGE_MULTIPLIER = -1;
19+
1820
export default (isSmallScreenWidth: boolean, themeStyles: ThemeStyles): ScreenOptions => ({
1921
rightModalNavigator: {
2022
...commonScreenOptions,
@@ -32,7 +34,22 @@ export default (isSmallScreenWidth: boolean, themeStyles: ThemeStyles): ScreenOp
3234
right: 0,
3335
},
3436
},
37+
leftModalNavigator: {
38+
...commonScreenOptions,
39+
cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, false, props, SLIDE_LEFT_OUTPUT_RANGE_MULTIPLIER),
40+
presentation: 'transparentModal',
41+
42+
// We want pop in LHP since there are some flows that would work weird otherwise
43+
animationTypeForReplace: 'pop',
44+
cardStyle: {
45+
...getNavigationModalCardStyle(),
46+
47+
// This is necessary to cover translated sidebar with overlay.
48+
width: isSmallScreenWidth ? '100%' : '200%',
3549

50+
transform: [{translateX: isSmallScreenWidth ? 0 : -variables.sideBarWidth}],
51+
},
52+
},
3653
homeScreen: {
3754
title: CONFIG.SITE_TITLE,
3855
...commonScreenOptions,

src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ import {Animated} from 'react-native';
33
import getCardStyles from '@styles/utils/cardStyles';
44
import variables from '@styles/variables';
55

6-
export default (isSmallScreenWidth: boolean, isFullScreenModal: boolean, {current: {progress}, inverted, layouts: {screen}}: StackCardInterpolationProps): StackCardInterpolatedStyle => {
6+
export default (
7+
isSmallScreenWidth: boolean,
8+
isFullScreenModal: boolean,
9+
{current: {progress}, inverted, layouts: {screen}}: StackCardInterpolationProps,
10+
outputRangeMultiplier = 1,
11+
): StackCardInterpolatedStyle => {
712
const translateX = Animated.multiply(
813
progress.interpolate({
914
inputRange: [0, 1],
10-
outputRange: [isSmallScreenWidth ? screen.width : variables.sideBarWidth, 0],
15+
outputRange: [outputRangeMultiplier * (isSmallScreenWidth ? screen.width : variables.sideBarWidth), 0],
1116
extrapolate: 'clamp',
1217
}),
1318
inverted,

src/libs/Navigation/Navigation.ts

Lines changed: 10 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
import {findFocusedRoute, getActionFromState} from '@react-navigation/core';
1+
import {findFocusedRoute} from '@react-navigation/core';
22
import {CommonActions, EventArg, getPathFromState, NavigationContainerEventMap, NavigationState, PartialState, StackActions} from '@react-navigation/native';
3-
import findLastIndex from 'lodash/findLastIndex';
43
import Log from '@libs/Log';
54
import CONST from '@src/CONST';
65
import NAVIGATORS from '@src/NAVIGATORS';
76
import ROUTES, {Route} from '@src/ROUTES';
8-
import SCREENS, {PROTECTED_SCREENS} from '@src/SCREENS';
9-
import getStateFromPath from './getStateFromPath';
7+
import {PROTECTED_SCREENS} from '@src/SCREENS';
8+
import originalDismissModal from './dismissModal';
109
import originalGetTopmostReportActionId from './getTopmostReportActionID';
1110
import originalGetTopmostReportId from './getTopmostReportId';
1211
import linkingConfig from './linkingConfig';
1312
import linkTo from './linkTo';
1413
import navigationRef from './navigationRef';
15-
import {StackNavigationAction, StateOrRoute} from './types';
14+
import {StateOrRoute} from './types';
1615

1716
let resolveNavigationIsReadyPromise: () => void;
1817
const navigationIsReadyPromise = new Promise<void>((resolve) => {
@@ -44,6 +43,9 @@ const getTopmostReportId = (state = navigationRef.getState()) => originalGetTopm
4443
// Re-exporting the getTopmostReportActionID here to fill in default value for state. The getTopmostReportActionID isn't defined in this file to avoid cyclic dependencies.
4544
const getTopmostReportActionId = (state = navigationRef.getState()) => originalGetTopmostReportActionId(state);
4645

46+
// Re-exporting the dismissModal here to fill in default value for navigationRef. The dismissModal isn't defined in this file to avoid cyclic dependencies.
47+
const dismissModal = (targetReportId = '', ref = navigationRef) => originalDismissModal(targetReportId, ref);
48+
4749
/** Method for finding on which index in stack we are. */
4850
function getActiveRouteIndex(stateOrRoute: StateOrRoute, index?: number): number | undefined {
4951
if ('routes' in stateOrRoute && stateOrRoute.routes) {
@@ -56,7 +58,7 @@ function getActiveRouteIndex(stateOrRoute: StateOrRoute, index?: number): number
5658
return getActiveRouteIndex(childActiveRoute, stateOrRoute.state.index ?? 0);
5759
}
5860

59-
if ('name' in stateOrRoute && stateOrRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
61+
if ('name' in stateOrRoute && (stateOrRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR || stateOrRoute.name === NAVIGATORS.LEFT_MODAL_NAVIGATOR)) {
6062
return 0;
6163
}
6264

@@ -160,8 +162,8 @@ function goBack(fallbackRoute: Route, shouldEnforceFallback = false, shouldPopTo
160162
if (isFirstRouteInNavigator) {
161163
const rootState = navigationRef.getRootState();
162164
const lastRoute = rootState.routes.at(-1);
163-
// 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.
164-
if (lastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && (lastRoute.state?.index ?? 0) > 0) {
165+
// 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.
166+
if ((lastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR || lastRoute?.name === NAVIGATORS.LEFT_MODAL_NAVIGATOR) && (lastRoute.state?.index ?? 0) > 0) {
165167
navigationRef.current.goBack();
166168
return;
167169
}
@@ -200,45 +202,6 @@ function setParams(params: Record<string, unknown>, routeKey: string) {
200202
});
201203
}
202204

203-
/**
204-
* Dismisses the last modal stack if there is any
205-
*
206-
* @param targetReportID - The reportID to navigate to after dismissing the modal
207-
*/
208-
function dismissModal(targetReportID?: string) {
209-
if (!canNavigate('dismissModal')) {
210-
return;
211-
}
212-
const rootState = navigationRef.getRootState();
213-
const lastRoute = rootState.routes.at(-1);
214-
switch (lastRoute?.name) {
215-
case NAVIGATORS.RIGHT_MODAL_NAVIGATOR:
216-
case SCREENS.NOT_FOUND:
217-
case SCREENS.REPORT_ATTACHMENTS:
218-
// if we are not in the target report, we need to navigate to it after dismissing the modal
219-
if (targetReportID && targetReportID !== getTopmostReportId(rootState)) {
220-
const state = getStateFromPath(ROUTES.REPORT_WITH_ID.getRoute(targetReportID));
221-
222-
const action: StackNavigationAction = getActionFromState(state, linkingConfig.config);
223-
if (action) {
224-
action.type = 'REPLACE';
225-
navigationRef.current?.dispatch(action);
226-
}
227-
// If not-found page is in the route stack, we need to close it
228-
} else if (targetReportID && rootState.routes.some((route) => route.name === SCREENS.NOT_FOUND)) {
229-
const lastRouteIndex = rootState.routes.length - 1;
230-
const centralRouteIndex = findLastIndex(rootState.routes, (route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR);
231-
navigationRef.current?.dispatch({...StackActions.pop(lastRouteIndex - centralRouteIndex), target: rootState.key});
232-
} else {
233-
navigationRef.current?.dispatch({...StackActions.pop(), target: rootState.key});
234-
}
235-
break;
236-
default: {
237-
Log.hmmm('[Navigation] dismissModal failed because there is no modal stack to dismiss');
238-
}
239-
}
240-
}
241-
242205
/**
243206
* Returns the current active route without the URL params
244207
*/

src/libs/Navigation/dismissModal.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {getActionFromState} from '@react-navigation/core';
2+
import {NavigationContainerRef, StackActions} from '@react-navigation/native';
3+
import {findLastIndex} from 'lodash';
4+
import Log from '@libs/Log';
5+
import NAVIGATORS from '@src/NAVIGATORS';
6+
import ROUTES from '@src/ROUTES';
7+
import SCREENS from '@src/SCREENS';
8+
import getStateFromPath from './getStateFromPath';
9+
import getTopmostReportId from './getTopmostReportId';
10+
import linkingConfig from './linkingConfig';
11+
import {RootStackParamList, StackNavigationAction} from './types';
12+
13+
// This function is in a separate file than Navigation.js to avoid cyclic dependency.
14+
15+
/**
16+
* Dismisses the last modal stack if there is any
17+
*
18+
* @param targetReportID - The reportID to navigate to after dismissing the modal
19+
*/
20+
function dismissModal(targetReportID: string, navigationRef: NavigationContainerRef<RootStackParamList>) {
21+
if (!navigationRef.isReady()) {
22+
return;
23+
}
24+
25+
const state = navigationRef.getState();
26+
const lastRoute = state.routes.at(-1);
27+
switch (lastRoute?.name) {
28+
case NAVIGATORS.LEFT_MODAL_NAVIGATOR:
29+
case NAVIGATORS.RIGHT_MODAL_NAVIGATOR:
30+
case SCREENS.NOT_FOUND:
31+
case SCREENS.REPORT_ATTACHMENTS:
32+
// if we are not in the target report, we need to navigate to it after dismissing the modal
33+
if (targetReportID && targetReportID !== getTopmostReportId(state)) {
34+
const reportState = getStateFromPath(ROUTES.REPORT_WITH_ID.getRoute(targetReportID));
35+
36+
const action: StackNavigationAction = getActionFromState(reportState, linkingConfig.config);
37+
if (action) {
38+
action.type = 'REPLACE';
39+
navigationRef.dispatch(action);
40+
}
41+
// If not-found page is in the route stack, we need to close it
42+
} else if (targetReportID && state.routes.some((route) => route.name === SCREENS.NOT_FOUND)) {
43+
const lastRouteIndex = state.routes.length - 1;
44+
const centralRouteIndex = findLastIndex(state.routes, (route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR);
45+
navigationRef.dispatch({...StackActions.pop(lastRouteIndex - centralRouteIndex), target: state.key});
46+
} else {
47+
navigationRef.dispatch({...StackActions.pop(), target: state.key});
48+
}
49+
break;
50+
default: {
51+
Log.hmmm('[Navigation] dismissModal failed because there is no modal stack to dismiss');
52+
}
53+
}
54+
}
55+
56+
export default dismissModal;

0 commit comments

Comments
 (0)