diff --git a/contributingGuides/NAVIGATION.md b/contributingGuides/NAVIGATION.md index 6e2b5b779606..d7a94c2a4337 100644 --- a/contributingGuides/NAVIGATION.md +++ b/contributingGuides/NAVIGATION.md @@ -70,3 +70,117 @@ Using [react-freeze](https://github.com/software-mansion/react-freeze) allows us - To keep the navigation state that was present before changing the layout, we save the state on every change and use it for the `initialState` prop. Changing the layout means that every component inside `NavigationContainer` is mounted anew. + +## Why we need to use minimal action in the `linkTo` function + +### The problem +Let's assume that the user wants to navigate like this: + +``` +1. Settings_root - navigate > Profile +2. Profile - UP > Settings_root +3. Settings_root - navigate > About +4. About - browser back button > Settings_root +``` + +Without minimal action, expected behavior won't be achieved and the final screen will be `Profile`. + +Broken behavior is the outcome of two things: +1. Back button in the browser resets the navigation state with the state saved in step two. + +2. `Navigation.navigate` creates action with `getActionFromState` dispatched at the top level of the navigation hierarchy. + +The reason why `getActionFromState` provided by `react-navigation` is dispatched at the top level of the navigation hierarchy is that it doesn't know about current navigation state, only about desired one. + +In this example it doesn't know if the `RightModalNavigator` and `Settings` are already mounted. + + +The action for the first step looks like that: +```json +{ + "type": "NAVIGATE", + "payload": { + "name": "RightModalNavigator", + "params": { + "initial": true, + "screen": "Settings", + "params": { + "initial": true, + "screen": "Profile", + } + } + } +} +``` + + +That means, the params for the `RightModalNavigator` and `Settings` (also a navigator) will be filled with the information that we want to have the `Profile` screen in the state. + +```json +{ + "index": 2, + "routes": [ + { "name": "Home", }, + { + "name": "RightModalNavigator", + // here you can see that the params are filled with the information about structure that should be in the state. + "params": { + "initial": true, + "screen": "Settings", + "params": { + "initial": true, + "screen": "Settings_Profile", + "path": "/settings/profile" + } + }, + "state": { + "index": 0, + "routes": [ + { + "name": "Settings", + // Same here + "params": { + "initial": true, + "screen": "Settings_Profile", + "path": "/settings/profile" + }, + "state": { + "index": 0, + "routes": [ + { + "name": "Settings_Profile" + } + ] + } + } + ] + } + } + ] +} +``` + +This information will stay here even if we pop the `Profile` screen and navigate to `About` screen. + +Later on, when the user presses the browser back button expecting that the `Settings_root` screen will appear, the navigation state will be reset with information about the `Profile` screen in the params and this will be used as a source of truth for the navigation. + +### The solution + +If we can create simple action that will only push one screen to the existing navigator, we won't fill any params of the navigators. + +The `getMinimalAction` compares action generated by the `getActionFromState` with the current navigation state and tries to find the smallest action possible. + +The action for the first step created with `getMinimalAction` looks like this: + +```json +{ + "type": "NAVIGATE", + "payload": { + "name": "Settings_Profile" + }, + "target": "Settings-stack-key-xyz" +} +``` + +### Deeplinking +There is no minimal action for deeplinking directly to the `Profile` screen. But because the `Settings_root` is not on the stack, pressing UP will reset the params for navigators to the correct ones. \ No newline at end of file diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js index 088d1973570c..7b22475bc623 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js @@ -2,111 +2,91 @@ import React from 'react'; import {createStackNavigator} from '@react-navigation/stack'; import * as ModalStackNavigators from '../ModalStackNavigators'; -import defaultModalScreenOptions from '../defaultModalScreenOptions'; +import RHPScreenOptions from '../RHPScreenOptions'; const Stack = createStackNavigator(); function RigthModalNavigator() { return ( - + diff --git a/src/libs/Navigation/AppNavigator/defaultModalScreenOptions.js b/src/libs/Navigation/AppNavigator/RHPScreenOptions.js similarity index 73% rename from src/libs/Navigation/AppNavigator/defaultModalScreenOptions.js rename to src/libs/Navigation/AppNavigator/RHPScreenOptions.js index 03f5b1ba75e3..9aa984023f4f 100644 --- a/src/libs/Navigation/AppNavigator/defaultModalScreenOptions.js +++ b/src/libs/Navigation/AppNavigator/RHPScreenOptions.js @@ -1,12 +1,12 @@ import {CardStyleInterpolators} from '@react-navigation/stack'; import styles from '../../../styles/styles'; -const defaultModalScreenOptions = { +const RHPScreenOptions = { headerShown: false, - animationEnabled: true, + animationEnabled: false, gestureDirection: 'horizontal', cardStyle: styles.navigationScreenCardStyle, cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS, }; -export default defaultModalScreenOptions; +export default RHPScreenOptions; diff --git a/src/libs/Navigation/linkTo.js b/src/libs/Navigation/linkTo.js index 1d219c450ecc..ad6052ca4d09 100644 --- a/src/libs/Navigation/linkTo.js +++ b/src/libs/Navigation/linkTo.js @@ -5,6 +5,43 @@ import linkingConfig from './linkingConfig'; import getTopmostReportId from './getTopmostReportId'; import getStateFromPath from './getStateFromPath'; +/** + * Motivation for this function is described in NAVIGATION.md + * + * @param {Object} action action generated by getActionFromState + * @param {Object} state The root state + * @returns {{minimalAction: Object, targetName: String}} minimal action is the action that we should dispatch and targetName is the name of the target navigator. + * targetName name is necessary to determine if we are going to use REPLACE for navigating between RHP flows. + */ +function getMinimalAction(action, state) { + let currentAction = action; + let currentState = state; + let currentTargetKey = null; + let targetName = null; + + while (currentState.routes[currentState.index].name === currentAction.payload.name) { + if (!currentState.routes[currentState.index].state) { + break; + } + + targetName = currentState.routes[currentState.index].name; + currentState = currentState.routes[currentState.index].state; + + currentTargetKey = currentState.key; + + // Creating new smaller action + currentAction = { + type: currentAction.type, + payload: { + name: currentAction.payload.params.screen, + params: currentAction.payload.params.params, + }, + target: currentTargetKey, + }; + } + return {minimalAction: currentAction, targetName}; +} + export default function linkTo(navigation, path, type) { if (navigation === undefined) { throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?"); @@ -29,8 +66,8 @@ export default function linkTo(navigation, path, type) { if (action.payload.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR && getTopmostReportId(root.getState()) !== getTopmostReportId(state)) { action.type = 'PUSH'; - // If this action is navigating to the RightModalNavigator and the last route on the root navigator is also RightModalNavigator - // then we want to replace the current RHP state with new one + // If the type is UP, we deeplinked into one of the RHP flows and we want to replace the current screen with the previous one in the flow + // and at the same time we want the back button to go to the page we were before the deeplink } else if (type === 'UP') { action.type = 'REPLACE'; @@ -40,6 +77,18 @@ export default function linkTo(navigation, path, type) { } } + if (action.payload.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { + const {minimalAction, targetName} = getMinimalAction(action, navigation.getRootState()); + if (minimalAction) { + // If the target name is RHP that means this action is responsible for changing flow within the RHP e.g. from settings to search. In that case we want to use REPLACE. + if (targetName === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { + minimalAction.type = 'REPLACE'; + } + root.dispatch(minimalAction); + return; + } + } + if (action !== undefined) { root.dispatch(action); } else {