Skip to content

Commit

Permalink
implement TopLevelBottomTabBar and proper animations
Browse files Browse the repository at this point in the history
  • Loading branch information
adamgrzybowski committed Dec 20, 2024
1 parent c611aa2 commit 7438d33
Show file tree
Hide file tree
Showing 18 changed files with 298 additions and 135 deletions.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
28 changes: 15 additions & 13 deletions src/libs/Navigation/AppNavigator/AuthScreens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -367,21 +367,23 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
}, []);

// Animation is disabled when navigating to the sidebar screen
const getSplitNavigatorOptions = (route: RouteProp<AuthScreensParamList>) => {
if (!shouldUseNarrowLayout || !route?.params) {
const getWorkspaceSplitNavigatorOptions = ({route}: {route: RouteProp<AuthScreensParamList>}) => {
// 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,
};
};
Expand All @@ -393,12 +395,12 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
{/* This have to be the first navigator in auth screens. */}
<RootStack.Screen
name={NAVIGATORS.REPORTS_SPLIT_NAVIGATOR}
options={({route}) => getSplitNavigatorOptions(route)}
options={rootNavigatorOptions.fullScreen}
getComponent={loadReportSplitNavigator}
/>
<RootStack.Screen
name={NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR}
options={({route}) => getSplitNavigatorOptions(route)}
options={rootNavigatorOptions.fullScreen}
getComponent={loadSettingsSplitNavigator}
/>
<RootStack.Screen
Expand All @@ -409,7 +411,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
/>
<RootStack.Screen
name={NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR}
options={({route}) => getSplitNavigatorOptions(route)}
options={getWorkspaceSplitNavigatorOptions}
getComponent={loadWorkspaceSplitNavigator}
/>
<RootStack.Screen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import React from 'react';
import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen';
import createSplitNavigator from '@libs/Navigation/AppNavigator/createSplitNavigator';
import useRootNavigatorOptions from '@libs/Navigation/AppNavigator/useRootNavigatorOptions';
import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation';
import type {PlatformStackNavigationOptions} from '@libs/Navigation/PlatformStackNavigation/types';
import type {SettingsSplitNavigatorParamList} from '@libs/Navigation/types';
import SCREENS from '@src/SCREENS';
import type ReactComponentModule from '@src/types/utils/ReactComponentModule';
Expand Down Expand Up @@ -45,18 +43,11 @@ function SettingsSplitNavigator() {
options={rootNavigatorOptions.homeScreen}
/>
{Object.entries(CENTRAL_PANE_SETTINGS_SCREENS).map(([screenName, componentGetter]) => {
const options: PlatformStackNavigationOptions = {animation: undefined};

if (screenName === SCREENS.SETTINGS.WORKSPACES) {
options.animation = Animations.NONE;
}

return (
<Split.Screen
key={screenName}
name={screenName as keyof Screens}
getComponent={componentGetter}
options={options}
/>
);
})}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -31,10 +34,26 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = {

const Split = createSplitNavigator<WorkspaceSplitNavigatorParamList>();

function WorkspaceNavigator() {
const route = useRoute();
function WorkspaceSplitNavigator({route, navigation}: PlatformStackScreenProps<AuthScreensParamList, typeof NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR>) {
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 (
<FocusTrapForScreens>
<Split.Navigator
Expand All @@ -60,7 +79,7 @@ function WorkspaceNavigator() {
);
}

WorkspaceNavigator.displayName = 'WorkspaceNavigator';
WorkspaceSplitNavigator.displayName = 'WorkspaceSplitNavigator';

export {CENTRAL_PANE_WORKSPACE_SCREENS};
export default WorkspaceNavigator;
export default WorkspaceSplitNavigator;
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {memo, useCallback, useEffect, useState} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import {PressableWithFeedback} from '@components/Pressable';
Expand All @@ -9,11 +10,14 @@ import Text from '@components/Text';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useCurrentReportID from '@hooks/useCurrentReportID';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
import {getPreservedSplitNavigatorState} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveSplitNavigatorState';
import {isFullScreenName} from '@libs/Navigation/helpers';
import Navigation from '@libs/Navigation/Navigation';
import type {AuthScreensParamList, RootStackParamList, State} from '@libs/Navigation/types';
import type {AuthScreensParamList, RootStackParamList, State, WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as SearchQueryUtils from '@libs/SearchQueryUtils';
import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
Expand All @@ -23,13 +27,22 @@ import BottomTabAvatar from '@pages/home/sidebar/BottomTabAvatar';
import BottomTabBarFloatingActionButton from '@pages/home/sidebar/BottomTabBarFloatingActionButton';
import variables from '@styles/variables';
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 DebugTabView from './DebugTabView';

const BOTTOM_TABS = {
HOME: 'HOME',
SEARCH: 'SEARCH',
SETTINGS: 'SETTINGS',
} as const;

type BottomTabs = ValueOf<typeof BOTTOM_TABS>;

type BottomTabBarProps = {
selectedTab: string | undefined;
selectedTab: BottomTabs;
};

/**
Expand Down Expand Up @@ -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<BrickRoad>(() =>
getChatTabBrickRoad(activeWorkspaceID, currentReportID, reports, betas, policies, priorityMode, transactionViolations),
);
Expand All @@ -84,15 +98,15 @@ 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;
}

Navigation.navigate(ROUTES.HOME);
}, [selectedTab]);

const navigateToSearch = useCallback(() => {
if (selectedTab === SCREENS.SEARCH.CENTRAL_PANE) {
if (selectedTab === BOTTOM_TABS.SEARCH) {
return;
}
interceptAnonymousUser(() => {
Expand All @@ -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 && (
Expand All @@ -145,7 +218,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
<View>
<Icon
src={Expensicons.Inbox}
fill={selectedTab === SCREENS.HOME ? theme.iconMenu : theme.icon}
fill={selectedTab === BOTTOM_TABS.HOME ? theme.iconMenu : theme.icon}
width={variables.iconBottomBar}
height={variables.iconBottomBar}
/>
Expand All @@ -154,7 +227,13 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
)}
</View>
<Text
style={[styles.textSmall, styles.textAlignCenter, styles.mt1Half, selectedTab === SCREENS.HOME ? styles.textBold : styles.textSupporting, styles.bottomTabBarLabel]}
style={[
styles.textSmall,
styles.textAlignCenter,
styles.mt1Half,
selectedTab === BOTTOM_TABS.HOME ? styles.textBold : styles.textSupporting,
styles.bottomTabBarLabel,
]}
>
{translate('common.inbox')}
</Text>
Expand All @@ -169,7 +248,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
<View>
<Icon
src={Expensicons.MoneySearch}
fill={selectedTab === SCREENS.SEARCH.CENTRAL_PANE ? theme.iconMenu : theme.icon}
fill={selectedTab === BOTTOM_TABS.SEARCH ? theme.iconMenu : theme.icon}
width={variables.iconBottomBar}
height={variables.iconBottomBar}
/>
Expand All @@ -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')}
</Text>
</PressableWithFeedback>
<BottomTabAvatar isSelected={selectedTab === SCREENS.SETTINGS.ROOT} />
<BottomTabAvatar
isSelected={selectedTab === BOTTOM_TABS.SETTINGS}
onPress={showSettingsPage}
/>
<View style={[styles.flex1, styles.bottomTabBarItem]}>
<BottomTabBarFloatingActionButton />
</View>
Expand All @@ -198,3 +280,5 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
BottomTabBar.displayName = 'BottomTabBar';

export default memo(BottomTabBar);
export {BOTTOM_TABS};
export type {BottomTabs};
Loading

0 comments on commit 7438d33

Please sign in to comment.