Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Poc/split go up #105

Merged
merged 22 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4fe5789
Add template of goUp
WojtekBoman Sep 30, 2024
cf25855
Handle navigating back to sidebar screen
WojtekBoman Sep 30, 2024
e2a304e
Replace FullScreenNavigator with SplitNavigator
WojtekBoman Jun 18, 2024
a3f95c8
Rename Split to SplitStack
WojtekBoman Jun 19, 2024
665f795
Rename FullScreenNavigator to WorkspaceNavigator
WojtekBoman Jun 19, 2024
ab72ccc
Add BottomTabBar to WorkspaceInitialPage and SettingsWorkspaces
WojtekBoman Jul 25, 2024
a9d0bf8
Add todo comments for new navigation logic, adjust split navigators, …
WojtekBoman Sep 3, 2024
9c7967a
Refactor useActiveWorkspace
WojtekBoman Sep 7, 2024
af90faf
Add createSplitNavigator
WojtekBoman Sep 11, 2024
097a82a
improve routers
adamgrzybowski Sep 18, 2024
f398653
add setter to activeWorkspaceID context
adamgrzybowski Sep 18, 2024
58be5b5
Fix LHN paddings
WojtekBoman Sep 19, 2024
0f9efb9
remember state between tabs
adamgrzybowski Sep 20, 2024
9148db3
Test freezing split navigators
WojtekBoman Sep 24, 2024
d74d1eb
Fix flickering in frozen split navigators
WojtekBoman Sep 27, 2024
3d73e89
Fix types in getMinimalAction and goUp functions
WojtekBoman Oct 2, 2024
b2aedc2
Fix replacing a sidebar screen in split navigators
WojtekBoman Oct 2, 2024
38d466f
Adjust Navigation.resetToHome to work with SplitNavigators
WojtekBoman Oct 4, 2024
e2ad744
Replace some usages of resetToHome with goUp(ROUTES.HOME)
WojtekBoman Oct 7, 2024
53fe13e
Cleanup linkTo and getMinimalAction
WojtekBoman Oct 8, 2024
05d529a
Cleanup navigation changes
WojtekBoman Oct 8, 2024
ece8857
Remove mappings from linkingConfig
WojtekBoman Oct 8, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function adaptStateIfNecessary({state, options: {sidebarScreen, defaultCentralSc
const workspaceCentralPane = state.routes.at(-1);

// There should always be sidebarScreen screen in the state to make sure go back works properly if we deeplinkg to a subpage of settings.
if (!isAtLeastOneInState(state, sidebarScreen)) {
if (!isAtLeastOneInState(state, sidebarScreen) && !isNarrowLayout) {
// @ts-expect-error Updating read only property
// noinspection JSConstantReassignment
state.stale = true; // eslint-disable-line
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {ParamListBase, StackActionHelpers, StackNavigationState} from '@rea
import {createNavigatorFactory, useNavigationBuilder, useRoute} from '@react-navigation/native';
import type {StackNavigationEventMap, StackNavigationOptions} from '@react-navigation/stack';
import {StackView} from '@react-navigation/stack';
import React, {useMemo} from 'react';
import React from 'react';
import {View} from 'react-native';
import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
Expand All @@ -14,25 +14,10 @@ import type {SplitStackNavigatorProps, SplitStackNavigatorRouterOptions} from '.
import useHandleScreenResize from './useHandleScreenResize';
import usePrepareSplitStackNavigatorChildren from './usePrepareSplitStackNavigatorChildren';

function getStateToRender(state: StackNavigationState<ParamListBase>, isSmallScreenWidth: boolean): StackNavigationState<ParamListBase> {
const sidebarScreenRoute = state.routes.at(0);
const centralScreenRoutes = state.routes.slice(1);
const routes = isSmallScreenWidth ? state.routes.slice(-2) : [sidebarScreenRoute, ...centralScreenRoutes.slice(-2)];

// Routes passed to the state have to be defined
const definedRoutes = routes.filter((route) => route !== undefined);

return {
...state,
routes: definedRoutes,
index: routes.length - 1,
};
}

function SplitStackNavigator<ParamList extends ParamListBase>(props: SplitStackNavigatorProps<ParamList>) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {isSmallScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const screenOptions = getRootNavigatorScreenOptions(shouldUseNarrowLayout, styles, StyleUtils);

const children = usePrepareSplitStackNavigatorChildren(props.children, props.sidebarScreen, screenOptions.homeScreen);
Expand All @@ -58,16 +43,14 @@ function SplitStackNavigator<ParamList extends ParamListBase>(props: SplitStackN

useHandleScreenResize(navigation);

const stateToRender = useMemo(() => getStateToRender(state, isSmallScreenWidth), [state, isSmallScreenWidth]);

return (
<FocusTrapForScreens>
<View style={styles.rootNavigatorContainerStyles(shouldUseNarrowLayout)}>
<NavigationContent>
<StackView
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
state={stateToRender}
state={state}
descriptors={descriptors}
navigation={navigation}
/>
Expand Down
199 changes: 104 additions & 95 deletions src/libs/Navigation/Navigation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {findFocusedRoute} from '@react-navigation/core';
import type {EventArg, NavigationContainerEventMap} from '@react-navigation/native';
import {getActionFromState} from '@react-navigation/core';
import type {EventArg, NavigationAction, NavigationContainerEventMap} from '@react-navigation/native';
import {CommonActions, getPathFromState, StackActions} from '@react-navigation/native';
import type {OnyxEntry} from 'react-native-onyx';
import type {Writable} from 'type-fest';
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
import Log from '@libs/Log';
import {isCentralPaneName, removePolicyIDParamFromState} from '@libs/NavigationUtils';
import {removePolicyIDParamFromState} from '@libs/NavigationUtils';
import {shallowCompare} from '@libs/ObjectUtils';
import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils';
import * as ReportConnection from '@libs/ReportConnection';
import * as ReportUtils from '@libs/ReportUtils';
Expand All @@ -17,17 +20,32 @@
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import originalCloseRHPFlow from './closeRHPFlow';
import getPolicyIDFromState from './getPolicyIDFromState';
import getTopmostBottomTabRoute from './getTopmostBottomTabRoute';
import getStateFromPath from './getStateFromPath';
import getTopmostCentralPaneRoute from './getTopmostCentralPaneRoute';
import originalGetTopmostReportActionId from './getTopmostReportActionID';
import originalGetTopmostReportId from './getTopmostReportId';
import isReportOpenInRHP from './isReportOpenInRHP';
import linkingConfig from './linkingConfig';
import getMatchingBottomTabRouteForState from './linkingConfig/getMatchingBottomTabRouteForState';
import createSplitNavigator from './linkingConfig/createSplitNavigator';
import linkTo from './linkTo';
import getMinimalAction from './linkTo/getMinimalAction';
import navigationRef from './navigationRef';
import setNavigationActionToMicrotaskQueue from './setNavigationActionToMicrotaskQueue';
import type {NavigationStateRoute, RootStackParamList, State, StateOrRoute} from './types';
import type {NavigationPartialRoute, NavigationStateRoute, RootStackParamList, SplitNavigatorLHNScreen, SplitNavigatorParamListType, State, StateOrRoute} from './types';

const SPLIT_NAVIGATOR_TO_SIDEBAR_MAP: Record<keyof SplitNavigatorParamListType, SplitNavigatorLHNScreen> = {
[NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]: SCREENS.HOME,
[NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR]: SCREENS.SETTINGS.ROOT,
[NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR]: SCREENS.WORKSPACE.INITIAL,
};

function getSidebarScreenParams(splitNavigatorRoute: NavigationStateRoute) {
if (splitNavigatorRoute.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR) {
return splitNavigatorRoute.state?.routes?.at(0)?.params;
}

return undefined;
}

let resolveNavigationIsReadyPromise: () => void;
const navigationIsReadyPromise = new Promise<void>((resolve) => {
Expand Down Expand Up @@ -67,7 +85,7 @@
// Then we can pass the report as a param without getting it from the Onyx.

/** Method for finding on which index in stack we are. */
function getActiveRouteIndex(stateOrRoute: StateOrRoute, index?: number): number | undefined {

Check failure on line 88 in src/libs/Navigation/Navigation.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

'getActiveRouteIndex' is defined but never used
if ('routes' in stateOrRoute && stateOrRoute.routes) {
const childActiveRoute = stateOrRoute.routes[stateOrRoute.index ?? 0];
return getActiveRouteIndex(childActiveRoute, stateOrRoute.index ?? 0);
Expand Down Expand Up @@ -111,8 +129,8 @@
* @param path - Path that you are looking for.
* @return - Returns distance to path or -1 if the path is not found in root navigator.
*/
function getDistanceFromPathInRootNavigator(path?: string): number {
let currentState = navigationRef.getRootState();
function getDistanceFromPathInRootNavigator(state: State, path?: string): number {

Check failure on line 132 in src/libs/Navigation/Navigation.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

'getDistanceFromPathInRootNavigator' is defined but never used
let currentState = {...state};

for (let index = 0; index < 5; index++) {
if (!currentState.routes.length) {
Expand All @@ -126,7 +144,7 @@
return index;
}

currentState = {...currentState, routes: currentState.routes.slice(0, -1), index: currentState.index - 1};

Check failure on line 147 in src/libs/Navigation/Navigation.ts

View workflow job for this annotation

GitHub Actions / typecheck

Type '{ routes: NavigationRoute<ParamListBase, string>[] | PartialRoute<Route<string, object | undefined>>[]; index: number; ... 4 more ...; stale: false; }' is not assignable to type '{ key: string; index: number; routeNames: string[]; history?: unknown[] | undefined; routes: NavigationRoute<ParamListBase, string>[]; type: string; stale: false; } | { ...; }'.

Check failure on line 147 in src/libs/Navigation/Navigation.ts

View workflow job for this annotation

GitHub Actions / typecheck

'currentState.index' is possibly 'undefined'.
}

return -1;
Expand Down Expand Up @@ -185,29 +203,66 @@
return;
}
// linkTo(navigationRef.current, route, type, isActiveRoute(route));
linkTo(navigationRef.current, route, type, isActiveRoute(route));

Check failure on line 206 in src/libs/Navigation/Navigation.ts

View workflow job for this annotation

GitHub Actions / typecheck

Expected 2-3 arguments, but got 4.
}

function newGoBack(fallbackRoute?: Route, shouldEnforceFallback = false, shouldPopToTop = false) {
function doesRouteMatchToMinimalActionPayload(route: NavigationStateRoute | NavigationPartialRoute, minimalAction: Writable<NavigationAction>) {
if (!minimalAction.payload) {
return false;
}

if (!('name' in minimalAction.payload)) {
return false;
}

const areRouteNamesEqual = route.name === minimalAction.payload.name;

if (!areRouteNamesEqual) {
return false;
}

if (!('params' in minimalAction.payload)) {
return false;
}

// @TODO: Fix params comparison. When comparing split navigators params, it may happen that first one has parameters with the initial settings and the second one does not.
return shallowCompare(route.params as Record<string, string | undefined>, minimalAction.payload.params as Record<string, string | undefined>);
}

function goUp(fallbackRoute: Route) {
if (!canNavigate('goBack')) {
return;
}

if (!navigationRef.current?.canGoBack()) {
Log.hmmm('[Navigation] Unable to go back');
if (!navigationRef.current) {
Log.hmmm('[Navigation] Unable to go up');
return;
}
navigationRef.current.goBack();

if (fallbackRoute) {
/**
* Cases to handle:
* 1. RHP
* 2. fallbackRoute is in the current navigator
* 3. fallbackRoute is in the different navigator
* 4. fallbackRoute isn't present in the current state
*/
const rootState = navigationRef.current.getRootState();
const stateFromPath = getStateFromPath(fallbackRoute);
const action = getActionFromState(stateFromPath, linkingConfig.config);

if (!action) {
return;
}

const {action: minimalAction, targetState} = getMinimalAction(action, rootState);

if (minimalAction.type !== CONST.NAVIGATION.ACTION_TYPE.NAVIGATE || !targetState) {
return;
}

const indexOfFallbackRoute = targetState.routes.findLastIndex((route) => doesRouteMatchToMinimalActionPayload(route, minimalAction));

if (indexOfFallbackRoute === -1) {
const replaceAction = {...minimalAction, type: 'REPLACE'} as NavigationAction;
navigationRef.current.dispatch(replaceAction);
return;
}

const distanceToPop = targetState.routes.length - indexOfFallbackRoute - 1;
navigationRef.current.dispatch({...StackActions.pop(distanceToPop), target: targetState.key});
}

/**
Expand All @@ -215,7 +270,7 @@
* @param shouldEnforceFallback - Enforces navigation to fallback route
* @param shouldPopToTop - Should we navigate to LHN on back press
*/
function goBack(fallbackRoute?: Route, shouldEnforceFallback = false, shouldPopToTop = false) {

Check failure on line 273 in src/libs/Navigation/Navigation.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

'shouldEnforceFallback' is assigned a value but never used
if (!canNavigate('goBack')) {
return;
}
Expand All @@ -228,98 +283,52 @@
}
}

if (!navigationRef.current?.canGoBack()) {
Log.hmmm('[Navigation] Unable to go back');
return;
}

const isFirstRouteInNavigator = !getActiveRouteIndex(navigationRef.current.getState());
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 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;
}
}

if (shouldEnforceFallback || (isFirstRouteInNavigator && fallbackRoute)) {
navigate(fallbackRoute, 'REPLACE');
if (fallbackRoute) {
goUp(fallbackRoute);
return;
}

const isCentralPaneFocused = isCentralPaneName(findFocusedRoute(navigationRef.current.getState())?.name);
const distanceFromPathInRootNavigator = getDistanceFromPathInRootNavigator(fallbackRoute ?? '');

if (isCentralPaneFocused && fallbackRoute) {
// Allow CentralPane to use UP with fallback route if the path is not found in root navigator.
if (distanceFromPathInRootNavigator === -1) {
navigate(fallbackRoute, 'REPLACE');
return;
}
const rootState = navigationRef.current?.getRootState();
const lastRoute = rootState?.routes.at(-1);

// Add possibility to go back more than one screen in root navigator if that screen is on the stack.
if (distanceFromPathInRootNavigator > 0) {
navigationRef.current.dispatch(StackActions.pop(distanceFromPathInRootNavigator));
return;
}
}
const canGoBack = navigationRef.current?.canGoBack();

// If the central pane is focused, it's possible that we navigated from other central pane with different matching bottom tab.
if (isCentralPaneFocused) {
const rootState = navigationRef.getRootState();
const stateAfterPop = {routes: rootState.routes.slice(0, -1)} as State<RootStackParamList>;
const topmostCentralPaneRouteAfterPop = getTopmostCentralPaneRoute(stateAfterPop);

const topmostBottomTabRoute = getTopmostBottomTabRoute(rootState as State<RootStackParamList>);
const matchingBottomTabRoute = getMatchingBottomTabRouteForState(stateAfterPop);

// If the central pane is defined after the pop action, we need to check if it's synced with the bottom tab screen.
// If not, we need to pop to the bottom tab screen/screens to sync it with the new central pane.
if (topmostCentralPaneRouteAfterPop && topmostBottomTabRoute?.name !== matchingBottomTabRoute.name) {
const bottomTabNavigator = rootState.routes.find((item: NavigationStateRoute) => item.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR)?.state;

if (bottomTabNavigator && bottomTabNavigator.index) {
const matchingIndex = bottomTabNavigator.routes.findLastIndex((item) => item.name === matchingBottomTabRoute.name);
const indexToPop = matchingIndex !== -1 ? bottomTabNavigator.index - matchingIndex : undefined;
navigationRef.current.dispatch({...StackActions.pop(indexToPop), target: bottomTabNavigator?.key});
}
}
}

navigationRef.current.goBack();
}

/**
* Close the current screen and navigate to the route.
* If the current screen is the first screen in the navigator, we force using the fallback route to replace the current screen.
* It's useful in a case where we want to close an RHP and navigate to another RHP to prevent any blink effect.
*/
function closeAndNavigate(route: Route) {
if (!navigationRef.current) {
if (!canGoBack && lastRoute?.name.endsWith('SplitNavigator') && lastRoute?.state?.routes?.length === 1) {
const splitNavigatorName = lastRoute?.name as keyof SplitNavigatorParamListType;
const name = SPLIT_NAVIGATOR_TO_SIDEBAR_MAP[splitNavigatorName];
const params = getSidebarScreenParams(lastRoute);
navigationRef.dispatch({
type: 'REPLACE',
payload: {
name,
params,
},
});
return;
}

const isFirstRouteInNavigator = !getActiveRouteIndex(navigationRef.current.getState());
if (isFirstRouteInNavigator) {
goBack(route, true);
if (!canGoBack) {
Log.hmmm('[Navigation] Unable to go back');
return;
}
goBack();
navigate(route);

navigationRef.current?.goBack();
}

/**
* Reset the navigation state to Home page
*/
function resetToHome() {
const isNarrowLayout = getIsNarrowLayout();
const rootState = navigationRef.getRootState();
const bottomTabKey = rootState.routes.find((item: NavigationStateRoute) => item.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR)?.state?.key;
if (bottomTabKey) {
navigationRef.dispatch({...StackActions.popToTop(), target: bottomTabKey});
}
navigationRef.dispatch({...StackActions.popToTop(), target: rootState.key});
const splitNavigatorMainScreen = !isNarrowLayout
? {
name: SCREENS.REPORT,
}
: undefined;
const payload = createSplitNavigator({name: SCREENS.HOME}, splitNavigatorMainScreen);
navigationRef.dispatch({payload, type: 'REPLACE', target: rootState.key});
}

/**
Expand Down Expand Up @@ -487,7 +496,6 @@
getActiveRoute,
getActiveRouteWithoutParams,
getReportRHPActiveRoute,
closeAndNavigate,
goBack,
isNavigationReady,
setIsNavigationReady,
Expand All @@ -502,6 +510,7 @@
setNavigationActionToMicrotaskQueue,
getTopMostCentralPaneRouteFromRootState,
navigateToReportWithPolicyCheck,
goUp,
};

export {navigationRef};
9 changes: 7 additions & 2 deletions src/libs/Navigation/linkTo/getMinimalAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ import type {Writable} from 'type-fest';
import type {State} from '@navigation/types';
import type {ActionPayload} from './types';

type MinimalAction = {
action: Writable<NavigationAction>;
targetState: State | undefined;
};

/**
* Motivation for this function is described in NAVIGATION.md
*
* @param action action generated by getActionFromState
* @param state The root state
* @returns minimalAction minimal action is the action that we should dispatch
*/
function getMinimalAction(action: NavigationAction, state: NavigationState): Writable<NavigationAction> {
function getMinimalAction(action: NavigationAction, state: NavigationState): MinimalAction {
let currentAction: NavigationAction = action;
let currentState: State | undefined = state;
let currentTargetKey: string | undefined;
Expand All @@ -36,7 +41,7 @@ function getMinimalAction(action: NavigationAction, state: NavigationState): Wri
target: currentTargetKey,
};
}
return currentAction;
return {action: currentAction, targetState: currentState};
}

export default getMinimalAction;
2 changes: 1 addition & 1 deletion src/libs/Navigation/linkTo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,6 @@ export default function linkTo(navigation: NavigationContainerRef<RootStackParam
action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH;
}

const minimalAction = getMinimalAction(action, navigation.getRootState());
const {action: minimalAction} = getMinimalAction(action, navigation.getRootState());
navigation.dispatch(minimalAction);
}
Loading
Loading