Skip to content

Commit

Permalink
Merge pull request Expensify#32473 from software-mansion-labs/ideal-n…
Browse files Browse the repository at this point in the history
…av-lhp

Ideal nav LHP
  • Loading branch information
mountiny authored Dec 18, 2023
2 parents f017c6b + b54996f commit 648c000
Show file tree
Hide file tree
Showing 16 changed files with 198 additions and 77 deletions.
1 change: 1 addition & 0 deletions src/NAVIGATORS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* */
export default {
CENTRAL_PANE_NAVIGATOR: 'CentralPaneNavigator',
LEFT_MODAL_NAVIGATOR: 'LeftModalNavigator',
RIGHT_MODAL_NAVIGATOR: 'RightModalNavigator',
FULL_SCREEN_NAVIGATOR: 'FullScreenNavigator',
} as const;
4 changes: 3 additions & 1 deletion src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
7 changes: 7 additions & 0 deletions src/libs/Navigation/AppNavigator/AuthScreens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,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 = {
Expand Down Expand Up @@ -318,6 +319,12 @@ function AuthScreens({lastUpdateIDAppliedToClient, session, lastOpenedPublicRoom
component={RightModalNavigator}
listeners={modalScreenListeners}
/>
<RootStack.Screen
name={NAVIGATORS.LEFT_MODAL_NAVIGATOR}
options={screenOptions.leftModalNavigator}
component={LeftModalNavigator}
listeners={modalScreenListeners}
/>
<RootStack.Screen
name={SCREENS.DESKTOP_SIGN_IN_REDIRECT}
options={screenOptions.fullScreen}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import {CardStyleInterpolators, StackNavigationOptions} from '@react-navigation/
import {ThemeStyles} from '@styles/index';

/**
* RHP stack navigator screen options generator function
* Modal stack navigator screen options generator function
* @param themeStyles - The styles object
* @returns The screen options object
*/
const RHPScreenOptions = (themeStyles: ThemeStyles): StackNavigationOptions => ({
const ModalNavigatorScreenOptions = (themeStyles: ThemeStyles): StackNavigationOptions => ({
headerShown: false,
animationEnabled: true,
gestureDirection: 'horizontal',
cardStyle: themeStyles.navigationScreenCardStyle,
cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS,
});

export default RHPScreenOptions;
export default ModalNavigatorScreenOptions;
45 changes: 45 additions & 0 deletions src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx
Original file line number Diff line number Diff line change
@@ -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<AuthScreensParamList, typeof NAVIGATORS.LEFT_MODAL_NAVIGATOR>;

const Stack = createStackNavigator<LeftModalNavigatorParamList>();

function LeftModalNavigator({navigation}: LeftModalNavigatorProps) {
const styles = useThemeStyles();
const {isSmallScreenWidth} = useWindowDimensions();
const screenOptions = useMemo(() => ModalNavigatorScreenOptions(styles), [styles]);

return (
<NoDropZone>
{!isSmallScreenWidth && (
<Overlay
isModalOnTheLeft
onPress={navigation.goBack}
/>
)}
<View style={styles.LHPNavigatorContainer(isSmallScreenWidth)}>
<Stack.Navigator screenOptions={screenOptions}>
<Stack.Screen
name={SCREENS.LEFT_MODAL.SEARCH}
component={ModalStackNavigators.SearchModalStackNavigator}
/>
</Stack.Navigator>
</View>
</NoDropZone>
);
}

LeftModalNavigator.displayName = 'LeftModalNavigator';

export default LeftModalNavigator;
7 changes: 5 additions & 2 deletions src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Animated.View style={styles.overlayStyles(current)}>
<Animated.View style={styles.overlayStyles(current, isModalOnTheLeft)}>
<View style={[styles.flex1, styles.flexColumn]}>
{/* 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -18,7 +18,7 @@ const Stack = createStackNavigator<RightModalNavigatorParamList>();
function RightModalNavigator({navigation}: RightModalNavigatorProps) {
const styles = useThemeStyles();
const {isSmallScreenWidth} = useWindowDimensions();
const screenOptions = useMemo(() => RHPScreenOptions(styles), [styles]);
const screenOptions = useMemo(() => ModalNavigatorScreenOptions(styles), [styles]);

return (
<NoDropZone>
Expand All @@ -33,10 +33,6 @@ function RightModalNavigator({navigation}: RightModalNavigatorProps) {
name={SCREENS.RIGHT_MODAL.NEW_CHAT}
component={ModalStackNavigators.NewChatModalStackNavigator}
/>
<Stack.Screen
name={SCREENS.RIGHT_MODAL.SEARCH}
component={ModalStackNavigators.SearchModalStackNavigator}
/>
<Stack.Screen
name={SCREENS.RIGHT_MODAL.DETAILS}
component={ModalStackNavigators.DetailsModalStackNavigator}
Expand Down
17 changes: 17 additions & 0 deletions src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const commonScreenOptions: StackNavigationOptions = {
animationTypeForReplace: 'push',
};

const SLIDE_LEFT_OUTPUT_RANGE_MULTIPLIER = -1;

export default (isSmallScreenWidth: boolean, themeStyles: ThemeStyles): ScreenOptions => ({
rightModalNavigator: {
...commonScreenOptions,
Expand All @@ -32,7 +34,22 @@ 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%',

transform: [{translateX: isSmallScreenWidth ? 0 : -variables.sideBarWidth}],
},
},
homeScreen: {
title: CONFIG.SITE_TITLE,
...commonScreenOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
57 changes: 10 additions & 47 deletions src/libs/Navigation/Navigation.ts
Original file line number Diff line number Diff line change
@@ -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<void>((resolve) => {
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -200,45 +202,6 @@ function setParams(params: Record<string, unknown>, 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
*/
Expand Down
56 changes: 56 additions & 0 deletions src/libs/Navigation/dismissModal.ts
Original file line number Diff line number Diff line change
@@ -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<RootStackParamList>) {
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;
Loading

0 comments on commit 648c000

Please sign in to comment.