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,