diff --git a/src/App.js b/src/App.js
index 1d2e07345c24..27e8105c2189 100644
--- a/src/App.js
+++ b/src/App.js
@@ -26,6 +26,7 @@ import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsCo
import * as Session from './libs/actions/Session';
import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop';
import OnyxUpdateManager from './libs/actions/OnyxUpdateManager';
+import {SidebarNavigationContextProvider} from './pages/home/sidebar/SidebarNavigationContext';
// For easier debugging and development, when we are in web we expose Onyx to the window, so you can more easily set data into Onyx
if (window && Environment.isDevelopment()) {
@@ -64,6 +65,7 @@ function App() {
EnvironmentProvider,
ThemeProvider,
ThemeStylesProvider,
+ SidebarNavigationContextProvider,
]}
>
diff --git a/src/CONST.ts b/src/CONST.ts
index 3973d8839dd7..d3841189fce2 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -2702,19 +2702,29 @@ const CONST = {
DEFAULT_COORDINATE: [-122.4021, 37.7911],
STYLE_URL: 'mapbox://styles/expensify/cllcoiqds00cs01r80kp34tmq',
},
+
ONYX_UPDATE_TYPES: {
HTTPS: 'https',
PUSHER: 'pusher',
},
+
EVENTS: {
SCROLLING: 'scrolling',
},
+
HORIZONTAL_SPACER: {
DEFAULT_BORDER_BOTTOM_WIDTH: 1,
DEFAULT_MARGIN_VERTICAL: 8,
HIDDEN_MARGIN_VERTICAL: 0,
HIDDEN_BORDER_BOTTOM_WIDTH: 0,
},
+
+ GLOBAL_NAVIGATION_OPTION: {
+ HOME: 'home',
+ CHATS: 'chats',
+ SPEND: 'spend',
+ WORKSPACES: 'workspaces',
+ },
} as const;
export default CONST;
diff --git a/src/GLOBAL_NAVIGATION_MAPPING.ts b/src/GLOBAL_NAVIGATION_MAPPING.ts
new file mode 100644
index 000000000000..f879c508ff31
--- /dev/null
+++ b/src/GLOBAL_NAVIGATION_MAPPING.ts
@@ -0,0 +1,9 @@
+import CONST from './CONST';
+import SCREENS from './SCREENS';
+
+export default {
+ [CONST.GLOBAL_NAVIGATION_OPTION.HOME]: [SCREENS.HOME_OLDDOT],
+ [CONST.GLOBAL_NAVIGATION_OPTION.CHATS]: [SCREENS.REPORT],
+ [CONST.GLOBAL_NAVIGATION_OPTION.SPEND]: [SCREENS.EXPENSES_OLDDOT, SCREENS.REPORTS_OLDDOT, SCREENS.INSIGHTS_OLDDOT],
+ [CONST.GLOBAL_NAVIGATION_OPTION.WORKSPACES]: [SCREENS.INDIVIDUAL_WORKSPACES_OLDDOT, SCREENS.GROUPS_WORKSPACES_OLDDOT, SCREENS.CARDS_AND_DOMAINS_OLDDOT],
+} as const;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index b2dafa643b22..2b64dd9c5465 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -318,4 +318,17 @@ export default {
// These are some on-off routes that will be removed once they're no longer needed (see GH issues for details)
SAASTR: 'saastr',
SBE: 'sbe',
+
+ // Iframe screens from olddot
+ HOME_OLDDOT: 'home',
+
+ // Spend tab
+ EXPENSES_OLDDOT: 'expenses',
+ REPORTS_OLDDOT: 'reports',
+ INSIGHTS_OLDDOT: 'insights',
+
+ // Workspaces tab
+ INDIVIDUALS_OLDDOT: 'individual_workspaces',
+ GROUPS_OLDDOT: 'group_workspaces',
+ CARDS_AND_DOMAINS_OLDDOT: 'cards-and-domains',
} as const;
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index eb125a43c239..0346168f0407 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -24,4 +24,17 @@ export default {
SIGN_IN_WITH_APPLE_DESKTOP: 'AppleSignInDesktop',
SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop',
DESKTOP_SIGN_IN_REDIRECT: 'DesktopSignInRedirect',
+
+ // Iframe screens from olddot
+ HOME_OLDDOT: 'Home_OLDDOT',
+
+ // Spend tab
+ EXPENSES_OLDDOT: 'Expenses_OLDDOT',
+ REPORTS_OLDDOT: 'Reports_OLDDOT',
+ INSIGHTS_OLDDOT: 'Insights_OLDDOT',
+
+ // Workspaces tab
+ INDIVIDUAL_WORKSPACES_OLDDOT: 'IndividualWorkspaces_OLDDOT',
+ GROUPS_WORKSPACES_OLDDOT: 'GroupWorkspaces_OLDDOT',
+ CARDS_AND_DOMAINS_OLDDOT: 'CardsAndDomains_OLDDOT',
} as const;
diff --git a/src/components/EnvironmentBadge.js b/src/components/EnvironmentBadge.js
index af118a37f3b4..a83fcedfabe4 100644
--- a/src/components/EnvironmentBadge.js
+++ b/src/components/EnvironmentBadge.js
@@ -28,7 +28,7 @@ function EnvironmentBadge() {
success={environment === CONST.ENVIRONMENT.STAGING || environment === CONST.ENVIRONMENT.ADHOC}
error={environment !== CONST.ENVIRONMENT.STAGING && environment !== CONST.ENVIRONMENT.ADHOC}
text={text}
- badgeStyles={[styles.alignSelfEnd, styles.headerEnvBadge]}
+ badgeStyles={[styles.alignSelfEnd, styles.headerEnvBadge, styles.ml1]}
textStyles={[styles.headerEnvBadgeText]}
environment={environment}
/>
diff --git a/src/components/FloatingActionButton.js b/src/components/FloatingActionButton.js
index f1174988e955..d6f5b907ace0 100644
--- a/src/components/FloatingActionButton.js
+++ b/src/components/FloatingActionButton.js
@@ -9,6 +9,7 @@ import themeColors from '../styles/themes/default';
import Tooltip from './Tooltip';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
import PressableWithFeedback from './Pressable/PressableWithFeedback';
+import variables from '../styles/variables';
const AnimatedIcon = Animated.createAnimatedComponent(Icon);
AnimatedIcon.displayName = 'AnimatedIcon';
@@ -100,6 +101,8 @@ class FloatingActionButton extends PureComponent {
style={[styles.floatingActionButton, StyleUtils.getAnimatedFABStyle(rotate, backgroundColor)]}
>
diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js
index fb6fd59870a0..ac37947daa17 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.js
+++ b/src/components/LHNOptionsList/OptionRowLHN.js
@@ -56,7 +56,7 @@ const propTypes = {
};
const defaultProps = {
- hoverStyle: styles.sidebarLinkHover,
+ hoverStyle: styles.sidebarLinkHoverLHN,
viewMode: 'default',
onSelectRow: () => {},
style: null,
@@ -110,7 +110,7 @@ function OptionRowLHN(props) {
: [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRow, styles.justifyContentCenter],
);
const hoveredBackgroundColor = props.hoverStyle && props.hoverStyle.backgroundColor ? props.hoverStyle.backgroundColor : themeColors.sidebar;
- const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor;
+ const focusedBackgroundColor = styles.sidebarLinkActiveLHN.backgroundColor;
const hasBrickError = optionItem.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR;
const defaultSubscriptSize = optionItem.isExpenseRequest ? CONST.AVATAR_SIZE.SMALL_NORMAL : CONST.AVATAR_SIZE.DEFAULT;
@@ -185,8 +185,8 @@ function OptionRowLHN(props) {
styles.flexRow,
styles.alignItemsCenter,
styles.justifyContentBetween,
- styles.sidebarLink,
- styles.sidebarLinkInner,
+ styles.sidebarLinkLHN,
+ styles.sidebarLinkInnerLHN,
StyleUtils.getBackgroundColorStyle(themeColors.sidebar),
props.isFocused ? styles.sidebarLinkActive : null,
(hovered || isContextMenuActive) && !props.isFocused ? props.hoverStyle : null,
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 1446d9f5dbb6..7133ed88579e 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1816,4 +1816,7 @@ export default {
selectSuggestedAddress: 'Please select a suggested address or use current location',
},
},
+ globalNavigationOptions: {
+ chats: 'Chats',
+ },
} satisfies TranslationBase;
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 3eaf94b86706..a98ddfaff7d0 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -2300,4 +2300,7 @@ export default {
selectSuggestedAddress: 'Por favor, selecciona una dirección sugerida o usa la ubicación actual.',
},
},
+ globalNavigationOptions: {
+ chats: 'Chats',
+ },
} satisfies EnglishTranslation;
diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.js
index de6162685079..3e3dc59dcd80 100644
--- a/src/libs/Navigation/Navigation.js
+++ b/src/libs/Navigation/Navigation.js
@@ -10,6 +10,7 @@ import linkingConfig from './linkingConfig';
import navigationRef from './navigationRef';
import NAVIGATORS from '../../NAVIGATORS';
import originalGetTopmostReportId from './getTopmostReportId';
+import originalGetTopMostCentralPaneRouteName from './getTopMostCentralPaneRouteName';
import originalGetTopmostReportActionId from './getTopmostReportActionID';
import getStateFromPath from './getStateFromPath';
import SCREENS from '../../SCREENS';
@@ -47,6 +48,9 @@ function canNavigate(methodName, params = {}) {
// Re-exporting the getTopmostReportId here to fill in default value for state. The getTopmostReportId isn't defined in this file to avoid cyclic dependencies.
const getTopmostReportId = (state = navigationRef.getState()) => originalGetTopmostReportId(state);
+// Re-exporting the getTopMostCentralPaneRouteName here to fill in default value for state. The getTopMostCentralPaneRouteName isn't defined in this file to avoid cyclic dependencies.
+const getTopMostCentralPaneRouteName = (state = navigationRef.getState()) => originalGetTopMostCentralPaneRouteName(state);
+
// Re-exporting the getTopmostReportActionID here to fill in default value for state. The getTopmostReportActionID isn't defined in this file to avoid cyclic dependencies.
const getTopmostReportActionId = (state = navigationRef.getState()) => originalGetTopmostReportActionId(state);
@@ -272,6 +276,7 @@ export default {
setIsNavigationReady,
getTopmostReportId,
getRouteNameFromStateEvent,
+ getTopMostCentralPaneRouteName,
getTopmostReportActionId,
};
diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js
index 86f716e7ab22..34a52adfeca9 100644
--- a/src/libs/Navigation/NavigationRoot.js
+++ b/src/libs/Navigation/NavigationRoot.js
@@ -1,4 +1,4 @@
-import React, {useRef, useEffect} from 'react';
+import React, {useRef, useEffect, useContext} from 'react';
import PropTypes from 'prop-types';
import {NavigationContainer, DefaultTheme, getPathFromState} from '@react-navigation/native';
import {useSharedValue, useAnimatedReaction, interpolateColor, withTiming, withDelay, Easing, runOnJS} from 'react-native-reanimated';
@@ -11,6 +11,7 @@ import Log from '../Log';
import StatusBar from '../StatusBar';
import useCurrentReportID from '../../hooks/useCurrentReportID';
import useWindowDimensions from '../../hooks/useWindowDimensions';
+import {SidebarNavigationContext} from '../../pages/home/sidebar/SidebarNavigationContext';
// https://reactnavigation.org/docs/themes
const navigationTheme = {
@@ -53,6 +54,7 @@ function parseAndLogRoute(state) {
function NavigationRoot(props) {
useFlipper(navigationRef);
const firstRenderRef = useRef(true);
+ const globalNavigation = useContext(SidebarNavigationContext);
const {updateCurrentReportID} = useCurrentReportID();
const {isSmallScreenWidth} = useWindowDimensions();
@@ -128,6 +130,9 @@ function NavigationRoot(props) {
}, 0);
parseAndLogRoute(state);
animateStatusBarBackgroundColor();
+
+ // Update the global navigation to show the correct selected menu items.
+ globalNavigation.updateFromNavigationState(state);
};
return (
diff --git a/src/libs/Navigation/getTopMostCentralPaneRouteName.js b/src/libs/Navigation/getTopMostCentralPaneRouteName.js
new file mode 100644
index 000000000000..f833575a397a
--- /dev/null
+++ b/src/libs/Navigation/getTopMostCentralPaneRouteName.js
@@ -0,0 +1,32 @@
+import lodashFindLast from 'lodash/findLast';
+
+/**
+ * Find the name of top most central pane route.
+ *
+ * @param {Object} state - The react-navigation state
+ * @returns {String | undefined} - It's possible that there is no central pane in the state.
+ */
+function getTopMostCentralPaneRouteName(state) {
+ if (!state) {
+ return undefined;
+ }
+ const topmostCentralPane = lodashFindLast(state.routes, (route) => route.name === 'CentralPaneNavigator');
+
+ if (!topmostCentralPane) {
+ return undefined;
+ }
+
+ if (topmostCentralPane.state && topmostCentralPane.state.routes) {
+ // State may don't have index in some cases. But in this case there will be only one route in state.
+ return topmostCentralPane.state.routes[topmostCentralPane.state.index || 0].name;
+ }
+
+ if (topmostCentralPane.params) {
+ // State may don't have inner state in some cases (e.g generating actions from path). But in this case there will be params available.
+ return topmostCentralPane.params.screen;
+ }
+
+ return undefined;
+}
+
+export default getTopMostCentralPaneRouteName;
diff --git a/src/libs/Navigation/linkTo.js b/src/libs/Navigation/linkTo.js
index 884a8aa02190..fcb3bd5df9c5 100644
--- a/src/libs/Navigation/linkTo.js
+++ b/src/libs/Navigation/linkTo.js
@@ -5,6 +5,7 @@ import linkingConfig from './linkingConfig';
import getTopmostReportId from './getTopmostReportId';
import getStateFromPath from './getStateFromPath';
import CONST from '../../CONST';
+import getTopMostCentralPaneRouteName from './getTopMostCentralPaneRouteName';
/**
* Motivation for this function is described in NAVIGATION.md
@@ -61,12 +62,15 @@ export default function linkTo(navigation, path, type) {
// If action type is different than NAVIGATE we can't change it to the PUSH safely
if (action.type === CONST.NAVIGATION.ACTION_TYPE.NAVIGATE) {
+ // Make sure that we are pushing a screen that is not currently on top of the stack.
+ const shouldPushIfCentralPane =
+ action.payload.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR &&
+ (getTopMostCentralPaneRouteName(root.getState()) !== getTopMostCentralPaneRouteName(state) || getTopmostReportId(root.getState()) !== getTopmostReportId(state));
+
// In case if type is 'FORCED_UP' we replace current screen with the provided. This means the current screen no longer exists in the stack
if (type === CONST.NAVIGATION.TYPE.FORCED_UP) {
action.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE;
-
- // If this action is navigating to the report screen and the top most navigator is different from the one we want to navigate - PUSH the new screen to the top of the stack
- } else if (action.payload.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR && getTopmostReportId(root.getState()) !== getTopmostReportId(state)) {
+ } else if (shouldPushIfCentralPane) {
action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH;
// 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
diff --git a/src/pages/home/sidebar/GlobalNavigation/GlobalNavigationMenuItem.js b/src/pages/home/sidebar/GlobalNavigation/GlobalNavigationMenuItem.js
new file mode 100644
index 000000000000..d24960e80ff2
--- /dev/null
+++ b/src/pages/home/sidebar/GlobalNavigation/GlobalNavigationMenuItem.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import {View} from 'react-native';
+import PropTypes from 'prop-types';
+import Text from '../../../../components/Text';
+import styles from '../../../../styles/styles';
+import * as StyleUtils from '../../../../styles/StyleUtils';
+import Icon from '../../../../components/Icon';
+import CONST from '../../../../CONST';
+import variables from '../../../../styles/variables';
+import PressableWithFeedback from '../../../../components/Pressable/PressableWithFeedback';
+
+const propTypes = {
+ /** Icon to display */
+ icon: PropTypes.elementType,
+
+ /** Text to display for the item */
+ title: PropTypes.string,
+
+ /** Function to fire when component is pressed */
+ onPress: PropTypes.func,
+
+ /** Whether item is focused or active */
+ isFocused: PropTypes.bool,
+};
+
+const defaultProps = {
+ icon: undefined,
+ isFocused: false,
+ onPress: () => {},
+ title: '',
+};
+
+const GlobalNavigationMenuItem = React.forwardRef(({icon, title, isFocused, onPress}, ref) => (
+ !isFocused && onPress()}
+ style={styles.globalNavigationItemContainer}
+ ref={ref}
+ accessibilityRole={CONST.ACCESSIBILITY_ROLE.MENUITEM}
+ accessibilityLabel={title}
+ >
+ {({pressed}) => (
+
+
+
+
+
+ {title}
+
+
+
+ )}
+
+));
+
+GlobalNavigationMenuItem.propTypes = propTypes;
+GlobalNavigationMenuItem.defaultProps = defaultProps;
+GlobalNavigationMenuItem.displayName = 'GlobalNavigationMenuItem';
+
+export default GlobalNavigationMenuItem;
diff --git a/src/pages/home/sidebar/GlobalNavigation/index.js b/src/pages/home/sidebar/GlobalNavigation/index.js
new file mode 100644
index 000000000000..1a8e923d1ff6
--- /dev/null
+++ b/src/pages/home/sidebar/GlobalNavigation/index.js
@@ -0,0 +1,51 @@
+import React, {useMemo, useContext} from 'react';
+import {View} from 'react-native';
+import _ from 'underscore';
+import styles from '../../../../styles/styles';
+import * as Expensicons from '../../../../components/Icon/Expensicons';
+import CONST from '../../../../CONST';
+import Navigation from '../../../../libs/Navigation/Navigation';
+import ROUTES from '../../../../ROUTES';
+import useLocalize from '../../../../hooks/useLocalize';
+import GlobalNavigationMenuItem from './GlobalNavigationMenuItem';
+import {SidebarNavigationContext} from '../SidebarNavigationContext';
+import SignInOrAvatarWithOptionalStatus from '../SignInOrAvatarWithOptionalStatus';
+
+function GlobalNavigation() {
+ const sidebarNavigation = useContext(SidebarNavigationContext);
+ const {translate} = useLocalize();
+ const items = useMemo(
+ () => [
+ {
+ icon: Expensicons.ChatBubble,
+ text: translate('globalNavigationOptions.chats'),
+ value: CONST.GLOBAL_NAVIGATION_OPTION.CHATS,
+ onSelected: () => {
+ Navigation.navigate(ROUTES.REPORT);
+ },
+ },
+ ],
+ [translate],
+ );
+
+ return (
+
+
+
+ {_.map(items, (item) => (
+ item.onSelected(item.value)}
+ isFocused={sidebarNavigation.selectedGlobalNavigationOption === item.value}
+ />
+ ))}
+
+
+ );
+}
+
+GlobalNavigation.displayName = 'GlobalNavigation';
+
+export default GlobalNavigation;
diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js
index 32c224990df1..6b232cf31f40 100644
--- a/src/pages/home/sidebar/SidebarLinks.js
+++ b/src/pages/home/sidebar/SidebarLinks.js
@@ -17,23 +17,17 @@ import * as App from '../../../libs/actions/App';
import LHNOptionsList from '../../../components/LHNOptionsList/LHNOptionsList';
import SidebarUtils from '../../../libs/SidebarUtils';
import Header from '../../../components/Header';
-import defaultTheme from '../../../styles/themes/default';
import OptionsListSkeletonView from '../../../components/OptionsListSkeletonView';
-import variables from '../../../styles/variables';
-import LogoComponent from '../../../../assets/images/expensify-wordmark.svg';
import PressableWithoutFeedback from '../../../components/Pressable/PressableWithoutFeedback';
import * as Session from '../../../libs/actions/Session';
import KeyboardShortcut from '../../../libs/KeyboardShortcut';
import onyxSubscribe from '../../../libs/onyxSubscribe';
import * as ReportActionContextMenu from '../report/ContextMenu/ReportActionContextMenu';
-import SignInOrAvatarWithOptionalStatus from './SignInOrAvatarWithOptionalStatus';
+import Text from '../../../components/Text';
import useLocalize from '../../../hooks/useLocalize';
import useWindowDimensions from '../../../hooks/useWindowDimensions';
const basePropTypes = {
- /** Toggles the navigation menu open and closed */
- onLinkClick: PropTypes.func.isRequired,
-
/** Safe area insets required for mobile devices margins */
insets: safeAreaInsetPropTypes.isRequired,
};
@@ -149,17 +143,11 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority
return (
- }
+ title={{translate('globalNavigationOptions.chats')}}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
shouldShowEnvironmentBadge
/>
@@ -173,7 +161,6 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority
-
);
diff --git a/src/pages/home/sidebar/SidebarNavigationContext.js b/src/pages/home/sidebar/SidebarNavigationContext.js
new file mode 100644
index 000000000000..47bda74e052d
--- /dev/null
+++ b/src/pages/home/sidebar/SidebarNavigationContext.js
@@ -0,0 +1,50 @@
+import React, {useMemo, useCallback, useState} from 'react';
+import PropTypes from 'prop-types';
+import _ from 'underscore';
+import CONST from '../../../CONST';
+import Navigation from '../../../libs/Navigation/Navigation';
+import GLOBAL_NAVIGATION_MAPPING from '../../../GLOBAL_NAVIGATION_MAPPING';
+
+const propTypes = {
+ /** Children to wrap. The part of app that should have acces to this context */
+ children: PropTypes.node.isRequired,
+};
+
+const SidebarNavigationContext = React.createContext({
+ selectedGlobalNavigationOption: undefined,
+ selectedSubNavigationOption: undefined,
+ updateFromNavigationState: () => {},
+});
+
+const mapSubNavigationOptionToGlobalNavigationOption = (SubNavigationOption) =>
+ _.findKey(GLOBAL_NAVIGATION_MAPPING, (globalNavigationOptions) => globalNavigationOptions.includes(SubNavigationOption));
+
+function SidebarNavigationContextProvider({children}) {
+ const [selectedGlobalNavigationOption, setSelectedGlobalNavigationOption] = useState(CONST.GLOBAL_NAVIGATION_OPTION.CHATS);
+ const [selectedSubNavigationOption, setSelectedSubNavigationOption] = useState();
+
+ const updateFromNavigationState = useCallback((navigationState) => {
+ const topmostCentralPaneRouteName = Navigation.getTopMostCentralPaneRouteName(navigationState);
+ if (!topmostCentralPaneRouteName) {
+ return;
+ }
+
+ setSelectedSubNavigationOption(topmostCentralPaneRouteName);
+ setSelectedGlobalNavigationOption(mapSubNavigationOptionToGlobalNavigationOption(topmostCentralPaneRouteName));
+ }, []);
+
+ const contextValue = useMemo(
+ () => ({
+ selectedGlobalNavigationOption,
+ selectedSubNavigationOption,
+ updateFromNavigationState,
+ }),
+ [selectedGlobalNavigationOption, selectedSubNavigationOption, updateFromNavigationState],
+ );
+
+ return {children};
+}
+
+SidebarNavigationContextProvider.propTypes = propTypes;
+
+export {SidebarNavigationContextProvider, SidebarNavigationContext};
diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js
index 065fbb4d6f43..97f1f7eaee20 100644
--- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js
+++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js
@@ -1,13 +1,19 @@
-import React, {useEffect} from 'react';
+import React from 'react';
import {View} from 'react-native';
+import PropTypes from 'prop-types';
import styles from '../../../../styles/styles';
-import SidebarLinksData from '../SidebarLinksData';
import ScreenWrapper from '../../../../components/ScreenWrapper';
import Timing from '../../../../libs/actions/Timing';
import CONST from '../../../../CONST';
import Performance from '../../../../libs/Performance';
-import sidebarPropTypes from './sidebarPropTypes';
import * as Browser from '../../../../libs/Browser';
+import GlobalNavigation from '../GlobalNavigation';
+import SubNavigation from '../SubNavigation/SubNavigation';
+
+const propTypes = {
+ /** Children to wrap (floating button). */
+ children: PropTypes.node.isRequired,
+};
/**
* Function called when a pinned chat is selected.
@@ -18,11 +24,6 @@ const startTimer = () => {
};
function BaseSidebarScreen(props) {
- useEffect(() => {
- Performance.markStart(CONST.TIMING.SIDEBAR_LOADED);
- Timing.start(CONST.TIMING.SIDEBAR_LOADED, true);
- }, []);
-
return (
{({insets}) => (
<>
-
-
+
+
{props.children}
@@ -46,7 +47,7 @@ function BaseSidebarScreen(props) {
);
}
-BaseSidebarScreen.propTypes = sidebarPropTypes;
+BaseSidebarScreen.propTypes = propTypes;
BaseSidebarScreen.displayName = 'BaseSidebarScreen';
export default BaseSidebarScreen;
diff --git a/src/pages/home/sidebar/SidebarScreen/index.js b/src/pages/home/sidebar/SidebarScreen/index.js
index 86d85be6cb04..53640fffe559 100755
--- a/src/pages/home/sidebar/SidebarScreen/index.js
+++ b/src/pages/home/sidebar/SidebarScreen/index.js
@@ -1,5 +1,4 @@
import React, {useCallback, useRef} from 'react';
-import sidebarPropTypes from './sidebarPropTypes';
import BaseSidebarScreen from './BaseSidebarScreen';
import FloatingActionButtonAndPopover from './FloatingActionButtonAndPopover';
import FreezeWrapper from '../../../../libs/Navigation/FreezeWrapper';
@@ -49,7 +48,6 @@ function SidebarScreen(props) {
);
}
-SidebarScreen.propTypes = sidebarPropTypes;
SidebarScreen.displayName = 'SidebarScreen';
export default SidebarScreen;
diff --git a/src/pages/home/sidebar/SidebarScreen/index.native.js b/src/pages/home/sidebar/SidebarScreen/index.native.js
index bb791de12f32..35c8b876338f 100755
--- a/src/pages/home/sidebar/SidebarScreen/index.native.js
+++ b/src/pages/home/sidebar/SidebarScreen/index.native.js
@@ -1,5 +1,4 @@
import React from 'react';
-import sidebarPropTypes from './sidebarPropTypes';
import BaseSidebarScreen from './BaseSidebarScreen';
import FloatingActionButtonAndPopover from './FloatingActionButtonAndPopover';
import FreezeWrapper from '../../../../libs/Navigation/FreezeWrapper';
@@ -19,7 +18,6 @@ function SidebarScreen(props) {
);
}
-SidebarScreen.propTypes = sidebarPropTypes;
SidebarScreen.displayName = 'SidebarScreen';
export default SidebarScreen;
diff --git a/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js b/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js
deleted file mode 100644
index 61a9194bb1e5..000000000000
--- a/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import PropTypes from 'prop-types';
-
-const sidebarPropTypes = {
- /** Callback when onLayout of sidebar is called */
- onLayout: PropTypes.func,
-};
-export default sidebarPropTypes;
diff --git a/src/pages/home/sidebar/SubNavigation/SubNavigation.js b/src/pages/home/sidebar/SubNavigation/SubNavigation.js
new file mode 100644
index 000000000000..0c893b356099
--- /dev/null
+++ b/src/pages/home/sidebar/SubNavigation/SubNavigation.js
@@ -0,0 +1,38 @@
+import React, {useEffect} from 'react';
+import PropTypes from 'prop-types';
+import {View} from 'react-native';
+import styles from '../../../../styles/styles';
+import SidebarLinksData from '../SidebarLinksData';
+import Timing from '../../../../libs/actions/Timing';
+import CONST from '../../../../CONST';
+import Performance from '../../../../libs/Performance';
+import safeAreaInsetPropTypes from '../../../safeAreaInsetPropTypes';
+
+const propTypes = {
+ /** Function called when a pinned chat is selected. */
+ onLinkClick: PropTypes.func.isRequired,
+
+ /** Insets for SidebarLInksData */
+ insets: safeAreaInsetPropTypes.isRequired,
+};
+
+function SubNavigation({onLinkClick, insets}) {
+ useEffect(() => {
+ Performance.markStart(CONST.TIMING.SIDEBAR_LOADED);
+ Timing.start(CONST.TIMING.SIDEBAR_LOADED, true);
+ }, []);
+
+ return (
+
+
+
+ );
+}
+
+SubNavigation.propTypes = propTypes;
+SubNavigation.displayName = 'SubNavigation';
+
+export default SubNavigation;
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 70861bffd759..f36e552c2ecd 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -1234,6 +1234,20 @@ const styles = (theme) => ({
height: '100%',
},
+ sidebarHeaderContainer: {
+ flexDirection: 'row',
+ paddingHorizontal: 20,
+ paddingVertical: 19,
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+
+ subNavigationContainer: {
+ backgroundColor: theme.sidebar,
+ flex: 1,
+ borderTopLeftRadius: variables.componentBorderRadiusRounded,
+ },
+
sidebarAnimatedWrapperContainer: {
height: '100%',
position: 'absolute',
@@ -1269,8 +1283,7 @@ const styles = (theme) => ({
floatingActionButtonContainer: {
position: 'absolute',
- right: 20,
-
+ left: 16,
// The bottom of the floating action button should align with the bottom of the compose box.
// The value should be equal to the height + marginBottom + marginTop of chatItemComposeSecondaryRow
bottom: 25,
@@ -1278,8 +1291,8 @@ const styles = (theme) => ({
floatingActionButton: {
backgroundColor: theme.success,
- height: variables.componentSizeLarge,
- width: variables.componentSizeLarge,
+ height: variables.componentSizeNormal,
+ width: variables.componentSizeNormal,
borderRadius: 999,
alignItems: 'center',
justifyContent: 'center',
@@ -1331,7 +1344,7 @@ const styles = (theme) => ({
createMenuPositionSidebar: (windowHeight) => ({
horizontal: 18,
- vertical: windowHeight - 100,
+ vertical: windowHeight - 75,
}),
createMenuPositionProfile: (windowWidth) => ({
@@ -1399,6 +1412,13 @@ const styles = (theme) => ({
textDecorationLine: 'none',
},
+ sidebarLinkLHN: {
+ textDecorationLine: 'none',
+ marginLeft: 12,
+ marginRight: 12,
+ borderRadius: 8,
+ },
+
sidebarLinkInner: {
alignItems: 'center',
flexDirection: 'row',
@@ -1406,6 +1426,13 @@ const styles = (theme) => ({
paddingRight: 20,
},
+ sidebarLinkInnerLHN: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ paddingLeft: 8,
+ paddingRight: 8,
+ },
+
sidebarLinkText: {
color: theme.textSupporting,
fontSize: variables.fontSizeNormal,
@@ -1417,11 +1444,20 @@ const styles = (theme) => ({
backgroundColor: theme.sidebarHover,
},
+ sidebarLinkHoverLHN: {
+ backgroundColor: theme.highlightBG,
+ },
+
sidebarLinkActive: {
backgroundColor: theme.border,
textDecorationLine: 'none',
},
+ sidebarLinkActiveLHN: {
+ backgroundColor: theme.highlightBG,
+ textDecorationLine: 'none',
+ },
+
sidebarLinkTextBold: {
fontFamily: fontFamily.EXP_NEUE_BOLD,
fontWeight: fontWeightBold,
@@ -3693,6 +3729,34 @@ const styles = (theme) => ({
width: '100%',
},
+ globalNavigation: {
+ width: variables.globalNavigationWidth,
+ backgroundColor: theme.highlightBG,
+ },
+
+ globalNavigationMenuContainer: {
+ marginTop: 13,
+ },
+
+ globalAndSubNavigationContainer: {
+ backgroundColor: theme.highlightBG,
+ },
+
+ globalNavigationSelectionIndicator: (isFocused) => ({
+ width: 4,
+ height: 52,
+ borderTopRightRadius: variables.componentBorderRadiusRounded,
+ borderBottomRightRadius: variables.componentBorderRadiusRounded,
+ backgroundColor: isFocused ? theme.iconMenu : theme.transparent,
+ }),
+
+ globalNavigationMenuItem: (isFocused) => (isFocused ? {color: theme.text, fontWeight: fontWeightBold, fontFamily: fontFamily.EXP_NEUE_BOLD} : {color: theme.icon}),
+
+ globalNavigationItemContainer: {
+ width: variables.globalNavigationWidth,
+ height: variables.globalNavigationWidth,
+ },
+
walletCard: {
borderRadius: variables.componentBorderRadiusLarge,
position: 'relative',
diff --git a/src/styles/themes/default.js b/src/styles/themes/default.js
index 173fda328d1f..db4719f5548a 100644
--- a/src/styles/themes/default.js
+++ b/src/styles/themes/default.js
@@ -43,7 +43,7 @@ const darkTheme = {
hoverComponentBG: colors.darkHighlightBackground,
activeComponentBG: colors.darkBorders,
signInSidebar: colors.green800,
- sidebar: colors.darkHighlightBackground,
+ sidebar: colors.darkAppBackground,
sidebarHover: colors.darkAppBackground,
heading: colors.darkPrimaryText,
textLight: colors.darkPrimaryText,
diff --git a/src/styles/themes/light.js b/src/styles/themes/light.js
index c459f9f10da6..3c80eb589a07 100644
--- a/src/styles/themes/light.js
+++ b/src/styles/themes/light.js
@@ -41,7 +41,7 @@ const lightTheme = {
hoverComponentBG: colors.lightHighlightBackground,
activeComponentBG: colors.lightBorders,
signInSidebar: colors.green800,
- sidebar: colors.lightHighlightBackground,
+ sidebar: colors.lightAppBackground,
sidebarHover: colors.lightBorders,
heading: colors.lightPrimaryText,
textLight: colors.white,
diff --git a/src/styles/utilities/spacing.ts b/src/styles/utilities/spacing.ts
index a47efe504326..b635b7cc39a6 100644
--- a/src/styles/utilities/spacing.ts
+++ b/src/styles/utilities/spacing.ts
@@ -449,6 +449,10 @@ export default {
paddingTop: 20,
},
+ pt6: {
+ paddingTop: 24,
+ },
+
pt10: {
paddingTop: 40,
},
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index a7191ce5b002..5dbe573bea3d 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -80,7 +80,8 @@ export default {
mobileResponsiveWidthBreakpoint: 800,
tabletResponsiveWidthBreakpoint: 1024,
safeInsertPercentage: 0.7,
- sideBarWidth: 375,
+ globalNavigationWidth: 72,
+ sideBarWidth: 303 + 72,
pdfPageMaxWidth: 992,
tooltipzIndex: 10050,
gutterWidth: 12,
diff --git a/tests/utils/LHNTestUtils.js b/tests/utils/LHNTestUtils.js
index 92b8662da5a6..8689c7b65602 100644
--- a/tests/utils/LHNTestUtils.js
+++ b/tests/utils/LHNTestUtils.js
@@ -195,7 +195,6 @@ function MockedSidebarLinks({currentReportID}) {
return (
{}}
insets={{
top: 0,
left: 0,