Skip to content

Commit

Permalink
Merge pull request #28277 from adamgrzybowski/@swm/global-nav-menu-v1
Browse files Browse the repository at this point in the history
@swm/global nav menu v1
  • Loading branch information
Hayata Suenaga authored Oct 7, 2023
2 parents 168e7f7 + 9e87953 commit 389d7b0
Show file tree
Hide file tree
Showing 30 changed files with 408 additions and 58 deletions.
2 changes: 2 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -64,6 +65,7 @@ function App() {
EnvironmentProvider,
ThemeProvider,
ThemeStylesProvider,
SidebarNavigationContextProvider,
]}
>
<CustomStatusBar />
Expand Down
10 changes: 10 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
9 changes: 9 additions & 0 deletions src/GLOBAL_NAVIGATION_MAPPING.ts
Original file line number Diff line number Diff line change
@@ -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;
13 changes: 13 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
13 changes: 13 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 1 addition & 1 deletion src/components/EnvironmentBadge.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
Expand Down
3 changes: 3 additions & 0 deletions src/components/FloatingActionButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -100,6 +101,8 @@ class FloatingActionButton extends PureComponent {
style={[styles.floatingActionButton, StyleUtils.getAnimatedFABStyle(rotate, backgroundColor)]}
>
<AnimatedIcon
width={variables.iconSizeSmall}
height={variables.iconSizeSmall}
src={Expensicons.Plus}
fill={fill}
/>
Expand Down
8 changes: 4 additions & 4 deletions src/components/LHNOptionsList/OptionRowLHN.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const propTypes = {
};

const defaultProps = {
hoverStyle: styles.sidebarLinkHover,
hoverStyle: styles.sidebarLinkHoverLHN,
viewMode: 'default',
onSelectRow: () => {},
style: null,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1816,4 +1816,7 @@ export default {
selectSuggestedAddress: 'Please select a suggested address or use current location',
},
},
globalNavigationOptions: {
chats: 'Chats',
},
} satisfies TranslationBase;
3 changes: 3 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
5 changes: 5 additions & 0 deletions src/libs/Navigation/Navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -272,6 +276,7 @@ export default {
setIsNavigationReady,
getTopmostReportId,
getRouteNameFromStateEvent,
getTopMostCentralPaneRouteName,
getTopmostReportActionId,
};

Expand Down
7 changes: 6 additions & 1 deletion src/libs/Navigation/NavigationRoot.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 = {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 (
Expand Down
32 changes: 32 additions & 0 deletions src/libs/Navigation/getTopMostCentralPaneRouteName.js
Original file line number Diff line number Diff line change
@@ -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;
10 changes: 7 additions & 3 deletions src/libs/Navigation/linkTo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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) => (
<PressableWithFeedback
onPress={() => !isFocused && onPress()}
style={styles.globalNavigationItemContainer}
ref={ref}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.MENUITEM}
accessibilityLabel={title}
>
{({pressed}) => (
<View style={[styles.alignItemsCenter, styles.flexRow, styles.flex1]}>
<View style={styles.globalNavigationSelectionIndicator(isFocused)} />
<View style={[styles.flexColumn, styles.flex1, styles.alignItemsCenter, styles.mr1]}>
<Icon
additionalStyles={[styles.popoverMenuIcon]}
pressed={pressed}
src={icon}
fill={isFocused ? StyleUtils.getIconFillColor(CONST.BUTTON_STATES.DEFAULT, true) : StyleUtils.getIconFillColor()}
/>
<View style={[styles.mt1, styles.alignItemsCenter]}>
<Text style={[StyleUtils.getFontSizeStyle(variables.fontSizeExtraSmall), styles.globalNavigationMenuItem(isFocused)]}>{title}</Text>
</View>
</View>
</View>
)}
</PressableWithFeedback>
));

GlobalNavigationMenuItem.propTypes = propTypes;
GlobalNavigationMenuItem.defaultProps = defaultProps;
GlobalNavigationMenuItem.displayName = 'GlobalNavigationMenuItem';

export default GlobalNavigationMenuItem;
51 changes: 51 additions & 0 deletions src/pages/home/sidebar/GlobalNavigation/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<View style={[styles.ph5, styles.pv3, styles.alignItemsCenter, styles.h100, styles.globalNavigation]}>
<SignInOrAvatarWithOptionalStatus />
<View style={styles.globalNavigationMenuContainer}>
{_.map(items, (item) => (
<GlobalNavigationMenuItem
key={item.value}
icon={item.icon}
title={item.text}
onPress={() => item.onSelected(item.value)}
isFocused={sidebarNavigation.selectedGlobalNavigationOption === item.value}
/>
))}
</View>
</View>
);
}

GlobalNavigation.displayName = 'GlobalNavigation';

export default GlobalNavigation;
Loading

0 comments on commit 389d7b0

Please sign in to comment.