Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 71 additions & 31 deletions src/libs/Navigation/CustomActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import {
} from '@react-navigation/native';
import lodashGet from 'lodash/get';
import linkingConfig from './linkingConfig';
import navigationRef from './navigationRef';

/**
* Go back to the Main Drawer
* @param {Object} navigationRef
*/
function navigateBackToRootDrawer(navigationRef) {
function navigateBackToRootDrawer() {
let isLeavingNestedDrawerNavigator = false;

// This should take us to the first view of the modal's stack navigator
Expand Down Expand Up @@ -42,55 +43,94 @@ function navigateBackToRootDrawer(navigationRef) {
}

/**
* In order to create the desired browser navigation behavior on web and mobile web we need to replace any
* type: 'drawer' routes with a type: 'route' so that when pushing to a report screen we never show the
* drawer. We don't want to remove these since we always want the history length to increase by 1 whenever
* we are moving to a new report screen or back to a previous one. This is a workaround since
* react-navigation default behavior for a drawer is to skip pushing history states when navigating to the
* current route
* Extracts the route from state object. Note: In the context where this is used currently the method is dependable.
* However, as our navigation system grows in complexity we may need to revisit this to be sure it is returning the expected route object.
*
* @param {String} screenName
* @param {Object} params
* @param {String} path
* @param {Object} navigationRef
* @param {Object} state
* @return {Object}
*/
function getRouteFromState(state) {
return lodashGet(state, 'routes[0].state.routes[0]', {});
}

/**
* @param {Object} state
* @returns {Object}
*/
function getParamsFromState(state) {
return getRouteFromState(state).params || {};
}

/**
* @param {Object} state
* @returns {String}
*/
function getScreenNameFromState(state) {
return getRouteFromState(state).name || '';
}

/**
* @returns {Object}
*/
function getActiveState() {
// We use our RootState as the dispatch's state is relative to the active navigator and might not contain our active screen.
return navigationRef.current.getRootState();
}

/**
* Special accomodation must be made for navigating to a screen inside a DrawerNavigator (e.g. our ReportScreen). The web/mWeb default behavior when
* calling "navigate()" does not give us the browser history we would expect for a typical web paradigm (e.g. that navigating from one screen another
* should allow us to navigate back to the screen we were on previously). This custom action helps us get around these problems.
*
* More context here: https://github.com/react-navigation/react-navigation/issues/9744
*
* @param {String} route
* @returns {Function}
*/
function pushDrawerRoute(screenName, params, path, navigationRef) {
function pushDrawerRoute(route) {
return (currentState) => {
let state = currentState;
// Parse the state, name, and params from the new route we want to navigate to.
const newStateFromRoute = getStateFromPath(route, linkingConfig.config);
const newScreenName = getScreenNameFromState(newStateFromRoute);
const newScreenParams = getParamsFromState(newStateFromRoute);

// Avoid the navigation and refocus the report if we're trying to navigate to our active report
// We use our RootState as the dispatch's state is relative to the active navigator and might
// not contain our active report.
const rootState = navigationRef.current.getRootState();
const activeReportID = lodashGet(rootState, 'routes[0].state.routes[0].params.reportID', '');
if (state.type !== 'drawer') {
navigateBackToRootDrawer(navigationRef);
// When we are navigating away from a non-drawer navigator we need to first dismiss any screens pushed onto the main stack.
if (currentState.type !== 'drawer') {
navigateBackToRootDrawer();
}
if (activeReportID === params.reportID) {

// If we're trying to navigate to the same screen that is already active there's nothing more to do except close the drawer.
// This prevents unnecessary re-rendering the screen and adding duplicate items to the browser history.
const activeState = getActiveState();
const activeScreenName = getScreenNameFromState(activeState);
const activeScreenParams = getParamsFromState(activeState);
if (newScreenName === activeScreenName && _.isEqual(activeScreenParams, newScreenParams)) {
return DrawerActions.closeDrawer();
}

// When navigating from non Drawer navigator, get new state for the report and reset the navigation state.
if (state.type !== 'drawer') {
state = linkingConfig.getStateFromPath
? linkingConfig.getStateFromPath(path, linkingConfig.config)
: getStateFromPath(path, linkingConfig.config);
let state = currentState;

// When navigating from non-Drawer navigator we switch to using the new state generated from the provided route. If we are navigating away from a non-Drawer navigator the
// currentState will not have a history field to use. By using the state from the route we create a "fresh state" that we can use to setup the browser history again.
// Note: A current limitation with this is that navigating "back" won't display the routes we have cleared out e.g. SearchPage and the history effectively gets "reset".
if (currentState.type !== 'drawer') {
state = newStateFromRoute;
}

const screenRoute = {type: 'route', name: screenName};
const history = _.map([...(state.history || [screenRoute])], () => screenRoute);
const screenRoute = {type: 'route', name: newScreenName};
const history = _.map(state.history ? [...state.history] : [screenRoute], () => screenRoute);

// Force drawer to close and show the report screen
// Force drawer to close and show

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB: I think this is removed mistakenly or and show can be removed as well.

Suggested change
// Force drawer to close and show
// Force drawer to close

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yea, not a mistake because I didn't want to have the "report page" referenced here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see what you mean actually. It does kind of seem like an unfinished thought... "and show... what?"

history.push({
type: 'drawer',
status: 'closed',
});

return CommonActions.reset({
...state,
routes: [{
name: screenName,
params,
name: newScreenName,
params: newScreenParams,
}],
history,
});
Expand Down
59 changes: 39 additions & 20 deletions src/libs/Navigation/Navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,24 @@ import {Keyboard} from 'react-native';
import {
StackActions,
DrawerActions,
createNavigationContainerRef,
getPathFromState,
} from '@react-navigation/native';
import PropTypes from 'prop-types';
import Onyx from 'react-native-onyx';
import Log from '../Log';
import linkTo from './linkTo';
import ROUTES from '../../ROUTES';
import SCREENS from '../../SCREENS';
import CustomActions from './CustomActions';
import ONYXKEYS from '../../ONYXKEYS';
import linkingConfig from './linkingConfig';
import navigationRef from './navigationRef';

let isLoggedIn = false;
Onyx.connect({
key: ONYXKEYS.SESSION,
callback: val => isLoggedIn = Boolean(val && val.authToken),
});

const navigationRef = createNavigationContainerRef();

// This flag indicates that we're trying to deeplink to a report when react-navigation is not fully loaded yet.
// If true, this flag will cause the drawer to start in a closed state (which is not the default for small screens)
// so it doesn't cover the report we're trying to link to.
Expand All @@ -38,15 +35,29 @@ function setDidTapNotification() {
didTapNotificationBeforeReady = true;
}

/**
* @param {String} methodName
* @param {Object} params
* @returns {Boolean}
*/
function canNavigate(methodName, params = {}) {
if (navigationRef.isReady()) {
return true;
}

Log.hmmm(`[Navigation] ${methodName} failed because navigation ref was not yet ready`, params);
return false;
}

/**
* Opens the LHN drawer.
* @private
*/
function openDrawer() {
if (!navigationRef.isReady()) {
Log.hmmm('[Navigation] openDrawer failed because navigation ref was not yet ready');
if (!canNavigate('openDrawer')) {
return;
}

navigationRef.current.dispatch(DrawerActions.openDrawer());
Keyboard.dismiss();
}
Expand All @@ -56,10 +67,10 @@ function openDrawer() {
* @private
*/
function closeDrawer() {
if (!navigationRef.isReady()) {
Log.hmmm('[Navigation] closeDrawer failed because navigation ref was not yet ready');
if (!canNavigate('closeDrawer')) {
return;
}

navigationRef.current.dispatch(DrawerActions.closeDrawer());
}

Expand All @@ -79,8 +90,7 @@ function getDefaultDrawerState(isSmallScreenWidth) {
* @param {Boolean} shouldOpenDrawer
*/
function goBack(shouldOpenDrawer = true) {
if (!navigationRef.isReady()) {
Log.hmmm('[Navigation] goBack failed because navigation ref was not yet ready');
if (!canNavigate('goBack')) {
return;
}

Expand All @@ -95,13 +105,26 @@ function goBack(shouldOpenDrawer = true) {
navigationRef.current.goBack();
}

/**
* We navigate to the certains screens with a custom action so that we can preserve the browser history in web. react-navigation does not handle this well
* and only offers a "mobile" navigation paradigm e.g. in order to add a history item onto the browser history stack one would need to use the "push" action.
* However, this is not performant as it would keep stacking ReportScreen instances (which are quite expensive to render).
* We're also looking to see if we have a participants route since those also have a reportID param, but do not have the problem described above and should not use the custom action.
*
* @param {String} route
* @returns {Boolean}
*/
function isDrawerRoute(route) {
const {reportID, isParticipantsRoute} = ROUTES.parseReportRouteParams(route);
return reportID && !isParticipantsRoute;
}

/**
* Main navigation method for redirecting to a route.
* @param {String} route
*/
function navigate(route = ROUTES.HOME) {
if (!navigationRef.isReady()) {
Log.hmmm('[Navigation] navigate failed because navigation ref was not yet ready', {route});
if (!canNavigate('navigate', {route})) {
return;
}

Expand All @@ -117,11 +140,8 @@ function navigate(route = ROUTES.HOME) {
return;
}

// Navigate to the ReportScreen with a custom action so that we can preserve the history. We're looking to see if we
// have a participants route since those should go through linkTo() as they open a different screen.
const {reportID, isParticipantsRoute} = ROUTES.parseReportRouteParams(route);
if (reportID && !isParticipantsRoute) {
navigationRef.current.dispatch(CustomActions.pushDrawerRoute(SCREENS.REPORT, {reportID}, route, navigationRef));
if (isDrawerRoute(route)) {
navigationRef.current.dispatch(CustomActions.pushDrawerRoute(route));
return;
}

Expand All @@ -134,16 +154,15 @@ function navigate(route = ROUTES.HOME) {
* @param {Boolean} [shouldOpenDrawer]
*/
function dismissModal(shouldOpenDrawer = false) {
if (!navigationRef.isReady()) {
Log.hmmm('[Navigation] dismissModal failed because navigation ref was not yet ready');
if (!canNavigate('dismissModal')) {
return;
}

const normalizedShouldOpenDrawer = _.isBoolean(shouldOpenDrawer)
? shouldOpenDrawer
: false;

CustomActions.navigateBackToRootDrawer(navigationRef);
CustomActions.navigateBackToRootDrawer();
if (normalizedShouldOpenDrawer) {
openDrawer();
}
Expand Down
6 changes: 6 additions & 0 deletions src/libs/Navigation/navigationRef.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {
createNavigationContainerRef,
} from '@react-navigation/native';

const navigationRef = createNavigationContainerRef();
export default navigationRef;