From c611aa2e4587990e67038bbcc23bed93d76f6007 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Wed, 18 Dec 2024 18:25:02 +0100 Subject: [PATCH 1/2] fix copying params for sidebar in adaptStateIfNecessary --- .../AppNavigator/createSplitNavigator/SplitRouter.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts index 077dcdf56fe4..cef5dd95c418 100644 --- a/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts +++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts @@ -1,14 +1,12 @@ import type {CommonActions, ParamListBase, PartialState, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native'; import {StackActions, StackRouter} from '@react-navigation/native'; +import isEmpty from 'lodash/isEmpty'; import pick from 'lodash/pick'; -import Onyx from 'react-native-onyx'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import {getParamsFromRoute} from '@libs/Navigation/helpers'; import navigationRef from '@libs/Navigation/navigationRef'; import type {NavigationPartialRoute} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; -import ONYXKEYS from '@src/ONYXKEYS'; -import SCREENS from '@src/SCREENS'; import type {SplitNavigatorRouterOptions} from './types'; import {getPreservedSplitNavigatorState} from './usePreserveSplitNavigatorState'; @@ -43,7 +41,10 @@ function adaptStateIfNecessary({state, options: {sidebarScreen, defaultCentralSc // - defaultCentralScreen to cover central pane. if (!isAtLeastOneInState(state, sidebarScreen)) { const paramsFromRoute = getParamsFromRoute(sidebarScreen); - let params = pick(lastRoute?.params, paramsFromRoute); + const copiedParams = pick(lastRoute?.params, paramsFromRoute); + + // We don't want to get an empty object as params because it breaks some navigation logic when comparing if routes are the same. + const params = isEmpty(copiedParams) ? undefined : copiedParams; // @ts-expect-error Updating read only property // noinspection JSConstantReassignment From 7438d337b7ba43f6c6d6d3e33a3abb68753bbe83 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Fri, 20 Dec 2024 12:28:57 +0100 Subject: [PATCH 2/2] implement TopLevelBottomTabBar and proper animations --- src/CONST.ts | 1 + .../Navigation/AppNavigator/AuthScreens.tsx | 28 ++--- .../Navigators/SettingsSplitNavigator.tsx | 9 -- .../Navigators/WorkspaceSplitNavigator.tsx | 33 ++++-- .../BottomTabBar.tsx | 102 ++++++++++++++++-- .../CustomRouter.ts | 19 +++- .../GetStateForActionHandlers.ts | 53 ++++++++- .../TopLevelBottomTabBar.tsx | 49 +++++++++ .../createResponsiveStackNavigator/index.tsx | 4 +- .../createResponsiveStackNavigator/types.ts | 15 ++- src/pages/Search/SearchPage.tsx | 6 +- src/pages/Search/SearchPageBottomTab.tsx | 5 +- src/pages/home/sidebar/BottomTabAvatar.tsx | 75 ++----------- .../SidebarScreen/BaseSidebarScreen.tsx | 7 +- src/pages/settings/InitialSettingsPage.tsx | 5 +- src/pages/workspace/WorkspaceInitialPage.tsx | 5 +- src/pages/workspace/WorkspacesListPage.tsx | 10 +- src/styles/index.ts | 7 ++ 18 files changed, 298 insertions(+), 135 deletions(-) create mode 100644 src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/TopLevelBottomTabBar.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 0ba7cce2d972..05259e2180b2 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4621,6 +4621,7 @@ const CONST = { /** These action types are custom for RootNavigator */ SWITCH_POLICY_ID: 'SWITCH_POLICY_ID', DISMISS_MODAL: 'DISMISS_MODAL', + OPEN_WORKSPACE_SPLIT: 'OPEN_WORKSPACE_SPLIT', }, }, TIME_PERIOD: { diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index f8331eade1e0..a003ecd60423 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -25,7 +25,6 @@ import Log from '@libs/Log'; import NavBarManager from '@libs/NavBarManager'; import getCurrentUrl from '@libs/Navigation/currentUrl'; import {isOnboardingFlowName} from '@libs/Navigation/helpers'; -import SIDEBAR_TO_SPLIT from '@libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_SPLIT'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation'; import Presentation from '@libs/Navigation/PlatformStackNavigation/navigationOptions/presentation'; @@ -59,6 +58,7 @@ import type {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type ReactComponentModule from '@src/types/utils/ReactComponentModule'; import createResponsiveStackNavigator from './createResponsiveStackNavigator'; +import {workspaceSplitsWithoutEnteringAnimation} from './createResponsiveStackNavigator/GetStateForActionHandlers'; import defaultScreenOptions from './defaultScreenOptions'; import ExplanationModalNavigator from './Navigators/ExplanationModalNavigator'; import FeatureTrainingModalNavigator from './Navigators/FeatureTrainingModalNavigator'; @@ -367,21 +367,23 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie }, []); // Animation is disabled when navigating to the sidebar screen - const getSplitNavigatorOptions = (route: RouteProp) => { - if (!shouldUseNarrowLayout || !route?.params) { + const getWorkspaceSplitNavigatorOptions = ({route}: {route: RouteProp}) => { + // We don't need to do anything special for the wide screen. + if (!shouldUseNarrowLayout) { return rootNavigatorOptions.fullScreen; } - const screenName = 'screen' in route.params ? route.params.screen : undefined; - - if (!screenName) { - return rootNavigatorOptions.fullScreen; - } - - const animationEnabled = !Object.keys(SIDEBAR_TO_SPLIT).includes(screenName); + // On the narrow screen, we want to animate this navigator if it is opened from the settings split. + // If it is opened from other tab, we don't want to animate it on the entry. + // There is a hook inside the workspace navigator that changes animation to SLIDE_FROM_RIGHT after entering. + // This way it can be animated properly when going back to the settings split. + const animationEnabled = !workspaceSplitsWithoutEnteringAnimation.has(route.key); return { ...rootNavigatorOptions.fullScreen, + + // Allow swipe to go back from this split navigator to the settings navigator. + gestureEnabled: true, animation: animationEnabled ? Animations.SLIDE_FROM_RIGHT : Animations.NONE, }; }; @@ -393,12 +395,12 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie {/* This have to be the first navigator in auth screens. */} getSplitNavigatorOptions(route)} + options={rootNavigatorOptions.fullScreen} getComponent={loadReportSplitNavigator} /> getSplitNavigatorOptions(route)} + options={rootNavigatorOptions.fullScreen} getComponent={loadSettingsSplitNavigator} /> getSplitNavigatorOptions(route)} + options={getWorkspaceSplitNavigatorOptions} getComponent={loadWorkspaceSplitNavigator} /> {Object.entries(CENTRAL_PANE_SETTINGS_SCREENS).map(([screenName, componentGetter]) => { - const options: PlatformStackNavigationOptions = {animation: undefined}; - - if (screenName === SCREENS.SETTINGS.WORKSPACES) { - options.animation = Animations.NONE; - } - return ( ); })} diff --git a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx index 2d269e88ba16..4e93f95ec837 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx @@ -1,9 +1,12 @@ -import {useRoute} from '@react-navigation/native'; -import React from 'react'; +import React, {useEffect} from 'react'; import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen'; +import {workspaceSplitsWithoutEnteringAnimation} from '@libs/Navigation/AppNavigator/createResponsiveStackNavigator/GetStateForActionHandlers'; import createSplitNavigator from '@libs/Navigation/AppNavigator/createSplitNavigator'; import useRootNavigatorOptions from '@libs/Navigation/AppNavigator/useRootNavigatorOptions'; -import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; +import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {AuthScreensParamList, WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; +import type NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; import type ReactComponentModule from '@src/types/utils/ReactComponentModule'; @@ -31,10 +34,26 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = { const Split = createSplitNavigator(); -function WorkspaceNavigator() { - const route = useRoute(); +function WorkspaceSplitNavigator({route, navigation}: PlatformStackScreenProps) { const rootNavigatorOptions = useRootNavigatorOptions(); + useEffect(() => { + const unsubscribe = navigation.addListener('transitionEnd', () => { + // We want to call this function only once. + unsubscribe(); + + // If we open this screen from a different tab, then it won't have animation. + if (!workspaceSplitsWithoutEnteringAnimation.has(route.key)) { + return; + } + + // We want ot set animation after mounting so it will animate on going UP to the settings split. + navigation.setOptions({animation: Animations.SLIDE_FROM_RIGHT}); + }); + + return unsubscribe; + }, [navigation, route.key]); + return ( ; + type BottomTabBarProps = { - selectedTab: string | undefined; + selectedTab: BottomTabs; }; /** @@ -73,6 +86,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const [chatTabBrickRoad, setChatTabBrickRoad] = useState(() => getChatTabBrickRoad(activeWorkspaceID, currentReportID, reports, betas, policies, priorityMode, transactionViolations), ); @@ -84,7 +98,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { }, [activeWorkspaceID, transactionViolations, reports, reportActions, betas, policies, priorityMode, currentReportID]); const navigateToChats = useCallback(() => { - if (selectedTab === SCREENS.HOME) { + if (selectedTab === BOTTOM_TABS.HOME) { return; } @@ -92,7 +106,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { }, [selectedTab]); const navigateToSearch = useCallback(() => { - if (selectedTab === SCREENS.SEARCH.CENTRAL_PANE) { + if (selectedTab === BOTTOM_TABS.SEARCH) { return; } interceptAnonymousUser(() => { @@ -119,6 +133,65 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { }); }, [activeWorkspaceID, selectedTab]); + const showSettingsPage = useCallback(() => { + const rootState = navigationRef.getRootState(); + const topmostFullScreenRoute = rootState.routes.findLast((route) => isFullScreenName(route.name)); + + if (!topmostFullScreenRoute) { + return; + } + + const lastRouteOfTopmostFullScreenRoute = 'state' in topmostFullScreenRoute ? topmostFullScreenRoute.state?.routes.at(-1) : undefined; + + if (lastRouteOfTopmostFullScreenRoute && lastRouteOfTopmostFullScreenRoute.name === SCREENS.SETTINGS.WORKSPACES && shouldUseNarrowLayout) { + Navigation.goBack(ROUTES.SETTINGS); + return; + } + + if (topmostFullScreenRoute.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR) { + Navigation.goBack(ROUTES.SETTINGS); + return; + } + + interceptAnonymousUser(() => { + const lastSettingsOrWorkspaceNavigatorRoute = rootState.routes.findLast( + (rootRoute) => rootRoute.name === NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR || rootRoute.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, + ); + + // If there is a workspace navigator route, then we should open the workspace initial screen as it should be "remembered". + if (lastSettingsOrWorkspaceNavigatorRoute?.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR) { + const state = lastSettingsOrWorkspaceNavigatorRoute.state ?? getPreservedSplitNavigatorState(lastSettingsOrWorkspaceNavigatorRoute.key); + const params = state?.routes.at(0)?.params as WorkspaceSplitNavigatorParamList[typeof SCREENS.WORKSPACE.INITIAL]; + + // Screens of this navigator should always have policyID + if (params.policyID) { + // This action will put settings split under the workspace split to make sure that we can swipe back to settings split. + navigationRef.dispatch({ + type: CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT, + payload: { + policyID: params.policyID, + }, + }); + } + return; + } + + // If there is settings workspace screen in the settings navigator, then we should open the settings workspaces as it should be "remembered". + if ( + lastSettingsOrWorkspaceNavigatorRoute && + lastSettingsOrWorkspaceNavigatorRoute.state && + lastSettingsOrWorkspaceNavigatorRoute.state.routes.at(-1)?.name === SCREENS.SETTINGS.WORKSPACES + ) { + Navigation.navigate(ROUTES.SETTINGS_WORKSPACES); + return; + } + + // Otherwise we should simply open the settings navigator. + // This case also covers if there is no route to remember. + Navigation.navigate(ROUTES.SETTINGS); + }); + }, [shouldUseNarrowLayout]); + return ( <> {!!user?.isDebugModeEnabled && ( @@ -145,7 +218,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { @@ -154,7 +227,13 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { )} {translate('common.inbox')} @@ -169,7 +248,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { @@ -179,14 +258,17 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { styles.textSmall, styles.textAlignCenter, styles.mt1Half, - selectedTab === SCREENS.SEARCH.CENTRAL_PANE ? styles.textBold : styles.textSupporting, + selectedTab === BOTTOM_TABS.SEARCH ? styles.textBold : styles.textSupporting, styles.bottomTabBarLabel, ]} > {translate('common.search')} - + @@ -198,3 +280,5 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { BottomTabBar.displayName = 'BottomTabBar'; export default memo(BottomTabBar); +export {BOTTOM_TABS}; +export type {BottomTabs}; diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/CustomRouter.ts b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/CustomRouter.ts index 15aa6b0e9d14..f656f43ac06b 100644 --- a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/CustomRouter.ts +++ b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/CustomRouter.ts @@ -10,7 +10,19 @@ import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; import * as GetStateForActionHandlers from './GetStateForActionHandlers'; import syncBrowserHistory from './syncBrowserHistory'; -import type {CustomRouterAction, CustomRouterActionType, DismissModalActionType, PushActionType, ResponsiveStackNavigatorRouterOptions, SwitchPolicyIdActionType} from './types'; +import type { + CustomRouterAction, + CustomRouterActionType, + DismissModalActionType, + OpenWorkspaceSplitActionType, + PushActionType, + ResponsiveStackNavigatorRouterOptions, + SwitchPolicyIdActionType, +} from './types'; + +function isOpenWorkspaceSplitAction(action: CustomRouterAction): action is OpenWorkspaceSplitActionType { + return action.type === CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT; +} function isSwitchPolicyIdAction(action: CustomRouterAction): action is SwitchPolicyIdActionType { return action.type === CONST.NAVIGATION.ACTION_TYPE.SWITCH_POLICY_ID; @@ -59,10 +71,13 @@ function CustomRouter(options: ResponsiveStackNavigatorRouterOptions) { const stackRouter = StackRouter(options); const {setActiveWorkspaceID} = useActiveWorkspace(); - // @TODO: Make sure that everything works fine without compareAndAdaptState function. Probably with getMatchingFullScreenRoute. return { ...stackRouter, getStateForAction(state: StackNavigationState, action: CustomRouterAction, configOptions: RouterConfigOptions) { + if (isOpenWorkspaceSplitAction(action)) { + return GetStateForActionHandlers.handleOpenWorkspaceSplitAction(state, action, configOptions, stackRouter); + } + if (isSwitchPolicyIdAction(action)) { return GetStateForActionHandlers.handleSwitchPolicyID(state, action, configOptions, stackRouter, setActiveWorkspaceID); } diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/GetStateForActionHandlers.ts b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/GetStateForActionHandlers.ts index 1a952c0c6caa..b2dea13fbd4d 100644 --- a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/GetStateForActionHandlers.ts +++ b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/GetStateForActionHandlers.ts @@ -7,7 +7,7 @@ import type {RootStackParamList, State} from '@libs/Navigation/types'; import * as SearchQueryUtils from '@libs/SearchQueryUtils'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; -import type {PushActionType, SwitchPolicyIdActionType} from './types'; +import type {OpenWorkspaceSplitActionType, PushActionType, SwitchPolicyIdActionType} from './types'; const MODAL_ROUTES_TO_DISMISS: string[] = [ NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, @@ -24,6 +24,55 @@ const MODAL_ROUTES_TO_DISMISS: string[] = [ SCREENS.CONCIERGE, ]; +const workspaceSplitsWithoutEnteringAnimation = new Set(); + +/** + * Handles the OPEN_WORKSPACE_SPLIT action. + * If the user is on other tab than settings and the workspace split is "remembered", this action will called after pressing the settings tab. + * It will push the settings split navigator first and then push the workspace split navigator. + * This allows the user to swipe back on the iOS to the settings split navigator underneath. + */ +function handleOpenWorkspaceSplitAction( + state: StackNavigationState, + action: OpenWorkspaceSplitActionType, + configOptions: RouterConfigOptions, + stackRouter: Router, CommonActions.Action | StackActionType>, +) { + const actionToPushSettingsSplitNavigator = StackActions.push(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR, { + screen: SCREENS.SETTINGS.WORKSPACES, + }); + + const actionToPushWorkspaceSplitNavigator = StackActions.push(NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, { + screen: SCREENS.WORKSPACE.INITIAL, + params: { + policyID: action.payload.policyID, + }, + }); + + const stateWithSettingsSplitNavigator = stackRouter.getStateForAction(state, actionToPushSettingsSplitNavigator, configOptions); + + if (!stateWithSettingsSplitNavigator) { + return null; + } + + const rehydratedStateWithSettingsSplitNavigator = stackRouter.getRehydratedState(stateWithSettingsSplitNavigator, configOptions); + const stateWithWorkspaceSplitNavigator = stackRouter.getStateForAction(rehydratedStateWithSettingsSplitNavigator, actionToPushWorkspaceSplitNavigator, configOptions); + + if (!stateWithWorkspaceSplitNavigator) { + return null; + } + + const lastFullScreenRoute = stateWithWorkspaceSplitNavigator.routes.at(-1); + + if (lastFullScreenRoute?.key) { + // If the user opened the workspace split navigator from a different tab, we don't want to animate the entering transition. + // To make it feel like bottom tab navigator. + workspaceSplitsWithoutEnteringAnimation.add(lastFullScreenRoute.key); + } + + return stateWithWorkspaceSplitNavigator; +} + function handleSwitchPolicyID( state: StackNavigationState, action: SwitchPolicyIdActionType, @@ -151,4 +200,4 @@ function handleDismissModalAction( return stackRouter.getStateForAction(state, newAction, configOptions); } -export {handleDismissModalAction, handlePushReportAction, handlePushSearchPageAction, handleSwitchPolicyID}; +export {handleOpenWorkspaceSplitAction, handleDismissModalAction, handlePushReportAction, handlePushSearchPageAction, handleSwitchPolicyID, workspaceSplitsWithoutEnteringAnimation}; diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/TopLevelBottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/TopLevelBottomTabBar.tsx new file mode 100644 index 000000000000..af50b0c2a258 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/TopLevelBottomTabBar.tsx @@ -0,0 +1,49 @@ +import {findFocusedRoute, useNavigationState} from '@react-navigation/native'; +import React from 'react'; +import {View} from 'react-native'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; +import useThemeStyles from '@hooks/useThemeStyles'; +import BottomTabBar, {BOTTOM_TABS} from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar'; +import {isFullScreenName} from '@libs/Navigation/helpers'; +import SIDEBAR_TO_SPLIT from '@libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_SPLIT'; +import type {FullScreenName} from '@libs/Navigation/types'; +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; + +const FULLSCREEN_TO_TAB = { + [NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]: BOTTOM_TABS.HOME, + [SCREENS.SEARCH.CENTRAL_PANE]: BOTTOM_TABS.SEARCH, + [NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR]: BOTTOM_TABS.SETTINGS, + [NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR]: BOTTOM_TABS.SETTINGS, +}; + +const SCREENS_WITH_BOTTOM_TAB_BAR = [...Object.keys(SIDEBAR_TO_SPLIT), SCREENS.SEARCH.CENTRAL_PANE, SCREENS.SETTINGS.WORKSPACES]; + +/** + * Currently we are using the hybrid approach for the bottom tab bar. + * On wide screen we are using per screen bottom tab bar. It gives us more flexibility. We can display the bottom tab bar on any screen without any navigation structure constraints. + * On narrow layout we display the top level bottom tab bar. It allows us to implement proper animations between screens. + */ +function TopLevelBottomTabBar() { + const styles = useThemeStyles(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const topmostFullScreenRoute = useNavigationState((state) => state?.routes.findLast((route) => isFullScreenName(route.name))); + const {paddingBottom} = useStyledSafeAreaInsets(); + + // Home as fallback selected tab. + const selectedTab = FULLSCREEN_TO_TAB[(topmostFullScreenRoute?.name as FullScreenName) ?? NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]; + + // There always should be a focused screen. + const isScreenWithBottomTabFocused = useNavigationState((state) => SCREENS_WITH_BOTTOM_TAB_BAR.includes(findFocusedRoute(state)?.name ?? '')); + + const shouldDisplayTopLevelBottomTabBar = isScreenWithBottomTabFocused && shouldUseNarrowLayout; + + return ( + + + + ); +} + +export default TopLevelBottomTabBar; diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx index 5662b394339c..ab479bf0e773 100644 --- a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx +++ b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx @@ -1,11 +1,12 @@ -import type {ParamListBase} from '@react-navigation/native'; import {createNavigatorFactory} from '@react-navigation/native'; +import type {ParamListBase} from '@react-navigation/native'; import useNavigationResetOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetOnLayoutChange'; import {isFullScreenName} from '@libs/Navigation/helpers'; import createPlatformStackNavigatorComponent from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent'; import defaultPlatformStackScreenOptions from '@libs/Navigation/PlatformStackNavigation/defaultPlatformStackScreenOptions'; import type {CustomStateHookProps, PlatformStackNavigationEventMap, PlatformStackNavigationOptions, PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types'; import CustomRouter from './CustomRouter'; +import TopLevelBottomTabBar from './TopLevelBottomTabBar'; function useCustomRouterState({state}: CustomStateHookProps) { const lastSplitIndex = state.routes.findLastIndex((route) => isFullScreenName(route.name)); @@ -19,6 +20,7 @@ const ResponsiveStackNavigatorComponent = createPlatformStackNavigatorComponent( defaultScreenOptions: defaultPlatformStackScreenOptions, useCustomEffects: useNavigationResetOnLayoutChange, useCustomState: useCustomRouterState, + ExtraContent: TopLevelBottomTabBar, }); function createResponsiveStackNavigator() { diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/types.ts b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/types.ts index 9f19ede081cd..7c0071773495 100644 --- a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/types.ts +++ b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/types.ts @@ -9,7 +9,19 @@ type CustomRouterActionType = policyID: string; }; } - | {type: typeof CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL}; + | { + type: typeof CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL; + } + | { + type: typeof CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT; + payload: { + policyID: string; + }; + }; + +type OpenWorkspaceSplitActionType = CustomRouterActionType & { + type: typeof CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT; +}; type SwitchPolicyIdActionType = CustomRouterActionType & { type: typeof CONST.NAVIGATION.ACTION_TYPE.SWITCH_POLICY_ID; @@ -33,6 +45,7 @@ type ResponsiveStackNavigatorProps = DefaultNavigatorOptions )} - + } + bottomContent={} > {!selectionMode?.isEnabled ? ( <> diff --git a/src/pages/home/sidebar/BottomTabAvatar.tsx b/src/pages/home/sidebar/BottomTabAvatar.tsx index 28712438aea9..095e1fdb40df 100644 --- a/src/pages/home/sidebar/BottomTabAvatar.tsx +++ b/src/pages/home/sidebar/BottomTabAvatar.tsx @@ -1,94 +1,31 @@ -import {useRoute} from '@react-navigation/native'; -import React, {useCallback} from 'react'; +import React from 'react'; import {useOnyx} from 'react-native-onyx'; import {PressableWithFeedback} from '@components/Pressable'; import Text from '@components/Text'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {getPreservedSplitNavigatorState} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveSplitNavigatorState'; -import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; -import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import CONST from '@src/CONST'; -import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; import AvatarWithDelegateAvatar from './AvatarWithDelegateAvatar'; import AvatarWithOptionalStatus from './AvatarWithOptionalStatus'; import ProfileAvatarWithIndicator from './ProfileAvatarWithIndicator'; type BottomTabAvatarProps = { - /** Whether the create menu is open or not */ - isCreateMenuOpen?: boolean; - /** Whether the avatar is selected */ isSelected?: boolean; + + /** Function to call when the avatar is pressed */ + onPress: () => void; }; -function BottomTabAvatar({isCreateMenuOpen = false, isSelected = false}: BottomTabAvatarProps) { +function BottomTabAvatar({onPress, isSelected = false}: BottomTabAvatarProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const delegateEmail = account?.delegatedAccess?.delegate ?? ''; - const route = useRoute(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const emojiStatus = currentUserPersonalDetails?.status?.emojiCode ?? ''; - const {shouldUseNarrowLayout} = useResponsiveLayout(); - - const showSettingsPage = useCallback(() => { - if (isCreateMenuOpen) { - // Prevent opening Settings page when click profile avatar quickly after clicking FAB icon - return; - } - - if (route.name === SCREENS.SETTINGS.WORKSPACES && shouldUseNarrowLayout) { - Navigation.goBack(ROUTES.SETTINGS); - return; - } - - if (route.name === SCREENS.WORKSPACE.INITIAL) { - Navigation.goBack(ROUTES.SETTINGS); - if (shouldUseNarrowLayout) { - Navigation.navigate(ROUTES.SETTINGS, CONST.NAVIGATION.ACTION_TYPE.REPLACE); - } - return; - } - - interceptAnonymousUser(() => { - const rootState = navigationRef.getRootState(); - const lastSettingsOrWorkspaceNavigatorRoute = rootState.routes.findLast( - (rootRoute) => rootRoute.name === NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR || rootRoute.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, - ); - - // If there is a workspace navigator route, then we should open the workspace initial screen as it should be "remembered". - if (lastSettingsOrWorkspaceNavigatorRoute?.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR) { - const state = lastSettingsOrWorkspaceNavigatorRoute.state ?? getPreservedSplitNavigatorState(lastSettingsOrWorkspaceNavigatorRoute.key); - const params = state?.routes.at(0)?.params as WorkspaceSplitNavigatorParamList[typeof SCREENS.WORKSPACE.INITIAL]; - // Screens of this navigator should always have policyID - if (params.policyID) { - Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(params.policyID)); - } - return; - } - - // If there is settings workspace screen in the settings navigator, then we should open the settings workspaces as it should be "remembered". - if ( - lastSettingsOrWorkspaceNavigatorRoute && - lastSettingsOrWorkspaceNavigatorRoute.state && - lastSettingsOrWorkspaceNavigatorRoute.state.routes.at(-1)?.name === SCREENS.SETTINGS.WORKSPACES - ) { - Navigation.navigate(ROUTES.SETTINGS_WORKSPACES); - return; - } - - // Otherwise we should simply open the settings navigator. - // This case also covers if there is no route to remember. - Navigation.navigate(ROUTES.SETTINGS); - }); - }, [isCreateMenuOpen, shouldUseNarrowLayout, route.name]); let children; @@ -119,7 +56,7 @@ function BottomTabAvatar({isCreateMenuOpen = false, isSelected = false}: BottomT return ( { // Whether the active workspace or the "Everything" page is loaded const isWorkspaceOrEverythingLoaded = !!activeWorkspace || activeWorkspaceID === undefined; @@ -74,7 +75,7 @@ function BaseSidebarScreen() { type: CONST.NAVIGATION.ACTION_TYPE.REPLACE, }); updateLastAccessedWorkspace(undefined); - }, [activeWorkspace, activeWorkspaceID]); + }, [activeWorkspace, activeWorkspaceID, currentRoute.key]); const shouldDisplaySearch = shouldUseNarrowLayout; @@ -83,7 +84,7 @@ function BaseSidebarScreen() { shouldEnableKeyboardAvoidingView={false} style={[styles.sidebar, Browser.isMobile() ? styles.userSelectNone : {}]} testID={BaseSidebarScreen.displayName} - bottomContent={} + bottomContent={} > {({insets}) => ( <> diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index 77397737cb33..4e5b05b5321e 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -32,7 +32,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; import {resetExitSurveyForm} from '@libs/actions/ExitSurvey'; import * as CurrencyUtils from '@libs/CurrencyUtils'; -import BottomTabBar from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar'; +import BottomTabBar, {BOTTOM_TABS} from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar'; import getTopmostRouteName from '@libs/Navigation/helpers/getTopmostRouteName'; import Navigation from '@libs/Navigation/Navigation'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; @@ -50,7 +50,6 @@ import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; import type {Icon as TIcon} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -428,7 +427,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr } + bottomContent={} shouldEnableKeyboardAvoidingView={false} > {headerContent} diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 008077e013fa..adcd8f7981cf 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -23,7 +23,7 @@ import useWaitForNavigation from '@hooks/useWaitForNavigation'; import {isConnectionInProgress} from '@libs/actions/connections'; import * as CardUtils from '@libs/CardUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; -import BottomTabBar from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar'; +import BottomTabBar, {BOTTOM_TABS} from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar'; import getTopmostRouteName from '@libs/Navigation/helpers/getTopmostRouteName'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -36,7 +36,6 @@ import * as ReimbursementAccount from '@userActions/ReimbursementAccount'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; @@ -396,7 +395,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac : null} + bottomContent={shouldShowBottomTab ? : null} > (); const [policyNameToDelete, setPolicyNameToDelete] = useState(); @@ -406,7 +402,7 @@ function WorkspacesListPage() { shouldEnableMaxHeight testID={WorkspacesListPage.displayName} shouldShowOfflineIndicatorInWideScreen - bottomContent={shouldUseNarrowLayout && } + bottomContent={shouldUseNarrowLayout && } > } + bottomContent={shouldUseNarrowLayout && } > borderRadius: variables.componentBorderRadiusLarge, }, + topLevelBottomTabBar: (shouldDisplayTopLevelBottomTabBar: boolean, bottomSafeAreaOffset: number) => ({ + position: 'absolute', + width: '100%', + paddingBottom: bottomSafeAreaOffset, + bottom: shouldDisplayTopLevelBottomTabBar ? 0 : -(bottomSafeAreaOffset + variables.bottomTabHeight), + }), + bottomTabBarContainer: { flexDirection: 'row', height: variables.bottomTabHeight,