diff --git a/assets/images/home.svg b/assets/images/home.svg index 9d9302ab6319b..6bc6bf833f2c3 100644 --- a/assets/images/home.svg +++ b/assets/images/home.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/contributingGuides/NAVIGATION.md b/contributingGuides/NAVIGATION.md index 35b18e8b173b6..026c122790e1c 100644 --- a/contributingGuides/NAVIGATION.md +++ b/contributingGuides/NAVIGATION.md @@ -64,7 +64,7 @@ import Navigation from '@libs/Navigation/Navigation'; import ROUTES from '@src/ROUTES'; // Basic navigation to a route -Navigation.navigate(ROUTES.HOME); +Navigation.navigate(ROUTES.INBOX); // Navigation with parameters Navigation.navigate( @@ -1344,7 +1344,7 @@ import {ROUTES} from '@src/ROUTES'; Navigation.goBack(); // Back navigation with fallback -Navigation.goBack(ROUTES.HOME); +Navigation.goBack(ROUTES.INBOX); const reportID = 123; // Back navigation to a route with specific params diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 17ae75357d2cb..cc0a933af57de 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7944,6 +7944,7 @@ const CONST = { REPORTS: 'NavigationTabBar-Reports', WORKSPACES: 'NavigationTabBar-Workspaces', ACCOUNT: 'NavigationTabBar-Account', + HOME: 'NavigationTabBar-Home', FLOATING_ACTION_BUTTON: 'NavigationTabBar-FloatingActionButton', FLOATING_RECEIPT_BUTTON: 'NavigationTabBar-FloatingReceiptButton', }, diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d0bb5e0ed1e8d..10f1331eb97ba 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -60,7 +60,9 @@ const MULTIFACTOR_AUTHENTICATION_PROTECTED_ROUTES = { const ROUTES = { ...PUBLIC_SCREENS_ROUTES, // This route renders the list of reports. - HOME: 'home', + INBOX: 'home', + // @TODO: Rename it to 'home' and INBOX to 'inbox' when removing the newDotHome beta + HOME: 'home-page', // eslint-disable-next-line no-restricted-syntax -- Legacy route generation WORKSPACES_LIST: {route: 'workspaces', getRoute: (backTo?: string) => getUrlWithBackToParam('workspaces', backTo)}, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index c672d4e1a0c80..d43b2eddf91f8 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -6,6 +6,7 @@ import type DeepValueOf from './types/utils/DeepValueOf'; const PROTECTED_SCREENS = { HOME: 'Home', + INBOX: 'Inbox', CONCIERGE: 'Concierge', REPORT_ATTACHMENTS: 'ReportAttachments', REPORT_ADD_ATTACHMENT: 'ReportAddAttachment', diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx index 783fe383530a6..debb870f86493 100644 --- a/src/components/FloatingActionButton.tsx +++ b/src/components/FloatingActionButton.tsx @@ -6,6 +6,7 @@ import {View} from 'react-native'; import Animated, {Easing, interpolateColor, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import Svg, {Path} from 'react-native-svg'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -53,6 +54,8 @@ function FloatingActionButton({onPress, onLongPress, isActive, accessibilityLabe const {shouldUseNarrowLayout} = useResponsiveLayout(); const isLHBVisible = !shouldUseNarrowLayout; const {translate} = useLocalize(); + const {isBetaEnabled} = usePermissions(); + const isNewDotHomeEnabled = isBetaEnabled(CONST.BETAS.NEW_DOT_HOME); const fabSize = isLHBVisible ? variables.iconSizeSmall : variables.iconSizeNormal; @@ -94,7 +97,7 @@ function FloatingActionButton({onPress, onLongPress, isActive, accessibilityLabe onLongPress?.(event); }; - if (isLHBVisible) { + if (isLHBVisible || isNewDotHomeEnabled) { return ( generateReportID(), []); + const {isBetaEnabled} = usePermissions(); + const isNewDotHomeEnabled = isBetaEnabled(CONST.BETAS.NEW_DOT_HOME); + const policyChatForActivePolicySelector = useCallback( (reports: OnyxCollection) => { if (isEmptyObject(activePolicy) || !activePolicy?.isPolicyExpenseChatEnabled) { @@ -71,7 +75,7 @@ function BaseFloatingCameraButton({icon}: BaseFloatingCameraButtonProps) { styles.ph0, // Prevent text selection on touch devices (e.g. on long press) canUseTouchScreen() && styles.userSelectNone, - styles.floatingCameraButton, + isNewDotHomeEnabled ? styles.floatingCameraButtonAboveFab : styles.floatingCameraButton, ]} accessibilityLabel={translate('sidebarScreen.fabScanReceiptExplained')} onPress={onPress} diff --git a/src/components/FloatingGPSButton/index.native.tsx b/src/components/FloatingGPSButton/index.native.tsx index 0d8d4c2c8b87a..acc69ad000f4d 100644 --- a/src/components/FloatingGPSButton/index.native.tsx +++ b/src/components/FloatingGPSButton/index.native.tsx @@ -5,6 +5,7 @@ import {PressableWithoutFeedback} from '@components/Pressable'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; @@ -22,6 +23,9 @@ function FloatingGpsButton() { const {textMutedReversed} = useTheme(); const styles = useThemeStyles(); + const {isBetaEnabled} = usePermissions(); + const isNewDotHomeEnabled = isBetaEnabled(CONST.BETAS.NEW_DOT_HOME); + if (!gpsDraftDetails?.isTracking) { return null; } @@ -33,7 +37,7 @@ function FloatingGpsButton() { return ( { setIsUsingImportedState(true); - Navigation.navigate(ROUTES.HOME); + Navigation.navigate(ROUTES.INBOX); }) .catch((error) => { console.error('Error importing state:', error); diff --git a/src/components/ImportOnyxState/index.tsx b/src/components/ImportOnyxState/index.tsx index e6ed2316b108a..aaeadb76994c6 100644 --- a/src/components/ImportOnyxState/index.tsx +++ b/src/components/ImportOnyxState/index.tsx @@ -42,7 +42,7 @@ export default function ImportOnyxState({setIsLoading}: ImportOnyxStateProps) { }) .then(() => { setIsUsingImportedState(true); - Navigation.navigate(ROUTES.HOME); + Navigation.navigate(ROUTES.INBOX); }) .catch((error) => { console.error('Error importing state:', error); diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index e4a116e4b5c13..ecad604578c06 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -135,7 +135,7 @@ function KYCWall({ if (paymentMethod === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { openPersonalBankAccountSetupView({shouldSetUpUSBankAccount: isIOUReport(iouReport)}); } else if (paymentMethod === CONST.PAYMENT_METHODS.DEBIT_CARD) { - Navigation.navigate(addDebitCardRoute ?? ROUTES.HOME); + Navigation.navigate(addDebitCardRoute ?? ROUTES.INBOX); } else if (paymentMethod === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT || policy) { if (iouReport && isIOUReport(iouReport)) { const adminPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policy?.id}`]; diff --git a/src/components/Navigation/DebugTabView.tsx b/src/components/Navigation/DebugTabView.tsx index 2af72a9b11079..304dca6145c61 100644 --- a/src/components/Navigation/DebugTabView.tsx +++ b/src/components/Navigation/DebugTabView.tsx @@ -110,7 +110,7 @@ function DebugTabView({selectedTab, chatTabBrickRoad}: DebugTabViewProps) { const {orderedReportIDs} = useSidebarOrderedReports(); const message = useMemo((): TranslationPaths | undefined => { - if (selectedTab === NAVIGATION_TABS.HOME) { + if (selectedTab === NAVIGATION_TABS.INBOX) { if (chatTabBrickRoad === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO) { return 'debug.indicatorStatus.theresAReportAwaitingAction'; } @@ -124,7 +124,7 @@ function DebugTabView({selectedTab, chatTabBrickRoad}: DebugTabViewProps) { }, [selectedTab, chatTabBrickRoad, status]); const indicator = useMemo(() => { - if (selectedTab === NAVIGATION_TABS.HOME) { + if (selectedTab === NAVIGATION_TABS.INBOX) { if (chatTabBrickRoad === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO) { return theme.success; } @@ -140,7 +140,7 @@ function DebugTabView({selectedTab, chatTabBrickRoad}: DebugTabViewProps) { }, [selectedTab, chatTabBrickRoad, theme.success, theme.danger, status, indicatorColor]); const navigateTo = useCallback(() => { - if (selectedTab === NAVIGATION_TABS.HOME && !!chatTabBrickRoad) { + if (selectedTab === NAVIGATION_TABS.INBOX && !!chatTabBrickRoad) { const reportID = getChatTabBrickRoadReportID(orderedReportIDs, reportAttributes); if (reportID) { @@ -156,7 +156,7 @@ function DebugTabView({selectedTab, chatTabBrickRoad}: DebugTabViewProps) { } }, [selectedTab, chatTabBrickRoad, orderedReportIDs, reportAttributes, status, reimbursementAccount, policyIDWithErrors]); - if (!([NAVIGATION_TABS.HOME, NAVIGATION_TABS.SETTINGS, NAVIGATION_TABS.WORKSPACES] as string[]).includes(selectedTab ?? '') || !indicator) { + if (!([NAVIGATION_TABS.INBOX, NAVIGATION_TABS.SETTINGS, NAVIGATION_TABS.WORKSPACES] as string[]).includes(selectedTab ?? '') || !indicator) { return null; } diff --git a/src/components/Navigation/NavigationTabBar/HomeNavigationTabBar.tsx b/src/components/Navigation/NavigationTabBar/HomeNavigationTabBar.tsx new file mode 100644 index 0000000000000..fa34e27dc461b --- /dev/null +++ b/src/components/Navigation/NavigationTabBar/HomeNavigationTabBar.tsx @@ -0,0 +1,611 @@ +import {findFocusedRoute, StackActions, useNavigationState} from '@react-navigation/native'; +import reportsSelector from '@selectors/Attributes'; +import React, {memo, useCallback, useEffect, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import type {OnyxCollection} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import FloatingCameraButton from '@components/FloatingCameraButton'; +import FloatingGPSButton from '@components/FloatingGPSButton'; +import Icon from '@components/Icon'; +import ImageSVG from '@components/ImageSVG'; +import DebugTabView from '@components/Navigation/DebugTabView'; +import {PressableWithFeedback} from '@components/Pressable'; +import Text from '@components/Text'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchTypeMenuSections from '@hooks/useSearchTypeMenuSections'; +import {useSidebarOrderedReports} from '@hooks/useSidebarOrderedReports'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useSubscriptionPlan from '@hooks/useSubscriptionPlan'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspacesTabIndicatorStatus from '@hooks/useWorkspacesTabIndicatorStatus'; +import clearSelectedText from '@libs/clearSelectedText/clearSelectedText'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import {getPreservedNavigatorState} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState'; +import getAccountTabScreenToOpen from '@libs/Navigation/helpers/getAccountTabScreenToOpen'; +import isRoutePreloaded from '@libs/Navigation/helpers/isRoutePreloaded'; +import navigateToWorkspacesPage, {getWorkspaceNavigationRouteState} from '@libs/Navigation/helpers/navigateToWorkspacesPage'; +import Navigation from '@libs/Navigation/Navigation'; +import {buildCannedSearchQuery, buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUtils'; +import {getDefaultActionableSearchMenuItem} from '@libs/SearchUIUtils'; +import {startSpan} from '@libs/telemetry/activeSpans'; +import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; +import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils'; +import navigationRef from '@navigation/navigationRef'; +import type {DomainSplitNavigatorParamList, RootNavigatorParamList, SearchFullscreenNavigatorParamList, State, WorkspaceSplitNavigatorParamList} from '@navigation/types'; +import NavigationTabBarAvatar from '@pages/home/sidebar/NavigationTabBarAvatar'; +import NavigationTabBarFloatingActionButton from '@pages/home/sidebar/NavigationTabBarFloatingActionButton'; +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 type {Domain, Policy} from '@src/types/onyx'; +import NAVIGATION_TABS from './NAVIGATION_TABS'; + +type NavigationTabBarProps = { + selectedTab: ValueOf; + isTopLevelBar?: boolean; + shouldShowFloatingButtons?: boolean; +}; + +function HomeNavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatingButtons = true}: NavigationTabBarProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + + const getIconFill = useCallback( + (isSelected: boolean, isHovered: boolean) => { + if (isSelected) { + return theme.iconMenu; + } + if (isHovered) { + return theme.success; + } + return theme.icon; + }, + [theme], + ); + const {translate, preferredLocale} = useLocalize(); + const {indicatorColor: workspacesTabIndicatorColor, status: workspacesTabIndicatorStatus} = useWorkspacesTabIndicatorStatus(); + const {orderedReportIDs} = useSidebarOrderedReports(); + const [isDebugModeEnabled] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED, {canBeMissing: true}); + const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true}); + const navigationState = useNavigationState(findFocusedRoute); + const initialNavigationRouteState = getWorkspaceNavigationRouteState(); + const [lastWorkspacesTabNavigatorRoute, setLastWorkspacesTabNavigatorRoute] = useState(initialNavigationRouteState.lastWorkspacesTabNavigatorRoute); + const [workspacesTabState, setWorkspacesTabState] = useState(initialNavigationRouteState.workspacesTabState); + const params = workspacesTabState?.routes?.at(0)?.params as + | WorkspaceSplitNavigatorParamList[typeof SCREENS.WORKSPACE.INITIAL] + | DomainSplitNavigatorParamList[typeof SCREENS.DOMAIN.INITIAL]; + const {typeMenuSections} = useSearchTypeMenuSections(); + const subscriptionPlan = useSubscriptionPlan(); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['ExpensifyAppIcon', 'Home', 'Inbox', 'MoneySearch', 'Buildings']); + + const paramsPolicyID = params && 'policyID' in params ? params.policyID : undefined; + const paramsDomainAccountID = params && 'domainAccountID' in params ? params.domainAccountID : undefined; + + const lastViewedPolicySelector = useCallback( + (policies: OnyxCollection) => { + if (!lastWorkspacesTabNavigatorRoute || lastWorkspacesTabNavigatorRoute.name !== NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR || !paramsPolicyID) { + return undefined; + } + + return policies?.[`${ONYXKEYS.COLLECTION.POLICY}${paramsPolicyID}`]; + }, + [paramsPolicyID, lastWorkspacesTabNavigatorRoute], + ); + + const [lastViewedPolicy] = useOnyx( + ONYXKEYS.COLLECTION.POLICY, + { + canBeMissing: true, + selector: lastViewedPolicySelector, + }, + [navigationState], + ); + + const lastViewedDomainSelector = useCallback( + (domains: OnyxCollection) => { + if (!lastWorkspacesTabNavigatorRoute || lastWorkspacesTabNavigatorRoute.name !== NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR || !paramsDomainAccountID) { + return undefined; + } + + return domains?.[`${ONYXKEYS.COLLECTION.DOMAIN}${paramsDomainAccountID}`]; + }, + [paramsDomainAccountID, lastWorkspacesTabNavigatorRoute], + ); + + const [lastViewedDomain] = useOnyx( + ONYXKEYS.COLLECTION.DOMAIN, + { + canBeMissing: true, + selector: lastViewedDomainSelector, + }, + [navigationState], + ); + + const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {selector: reportsSelector, canBeMissing: true}); + const {login: currentUserLogin} = useCurrentUserPersonalDetails(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const [chatTabBrickRoad, setChatTabBrickRoad] = useState(undefined); + + const StyleUtils = useStyleUtils(); + + useEffect(() => { + const newWorkspacesTabState = getWorkspaceNavigationRouteState(); + const newLastRoute = newWorkspacesTabState.lastWorkspacesTabNavigatorRoute; + const newTabState = newWorkspacesTabState.workspacesTabState; + + setLastWorkspacesTabNavigatorRoute(newLastRoute); + setWorkspacesTabState(newTabState); + }, [navigationState]); + + // On a wide layout DebugTabView should be rendered only within the navigation tab bar displayed directly on screens. + const shouldRenderDebugTabViewOnWideLayout = !!isDebugModeEnabled && !isTopLevelBar; + + useEffect(() => { + setChatTabBrickRoad(getChatTabBrickRoad(orderedReportIDs, reportAttributes)); + }, [orderedReportIDs, reportAttributes]); + + const navigateToNewDotHome = useCallback(() => { + if (selectedTab === NAVIGATION_TABS.HOME) { + return; + } + Navigation.navigate(ROUTES.HOME); + }, [selectedTab]); + + const navigateToChats = useCallback(() => { + if (selectedTab === NAVIGATION_TABS.INBOX) { + return; + } + + startSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB, { + name: CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB, + op: CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB, + }); + + if (!shouldUseNarrowLayout && isRoutePreloaded(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR)) { + // We use dispatch here because the correct screens and params are preloaded and set up in usePreloadFullScreenNavigators. + navigationRef.dispatch(StackActions.push(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR)); + return; + } + + Navigation.navigate(ROUTES.INBOX); + }, [selectedTab, shouldUseNarrowLayout]); + + const [lastSearchParams] = useOnyx(ONYXKEYS.REPORT_NAVIGATION_LAST_SEARCH_QUERY, {canBeMissing: true}); + + const navigateToSearch = useCallback(() => { + if (selectedTab === NAVIGATION_TABS.SEARCH) { + return; + } + clearSelectedText(); + interceptAnonymousUser(() => { + const parentSpan = startSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB, { + name: CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB, + op: CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB, + }); + + startSpan(CONST.TELEMETRY.SPAN_ON_LAYOUT_SKELETON_REPORTS, { + name: CONST.TELEMETRY.SPAN_ON_LAYOUT_SKELETON_REPORTS, + op: CONST.TELEMETRY.SPAN_ON_LAYOUT_SKELETON_REPORTS, + parentSpan, + }); + + const rootState = navigationRef.getRootState() as State; + const lastSearchNavigator = rootState.routes.findLast((route) => route.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR); + const lastSearchNavigatorState = lastSearchNavigator && lastSearchNavigator.key ? getPreservedNavigatorState(lastSearchNavigator?.key) : undefined; + const lastSearchRoute = lastSearchNavigatorState?.routes.findLast((route) => route.name === SCREENS.SEARCH.ROOT); + + if (lastSearchRoute) { + const {q, ...rest} = lastSearchRoute.params as SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.ROOT]; + const queryJSON = buildSearchQueryJSON(q); + if (queryJSON) { + const query = buildSearchQueryString(queryJSON); + Navigation.navigate( + ROUTES.SEARCH_ROOT.getRoute({ + query, + ...rest, + }), + ); + return; + } + } + + const flattenedMenuItems = typeMenuSections.flatMap((section) => section.menuItems); + const defaultActionableSearchQuery = + getDefaultActionableSearchMenuItem(flattenedMenuItems)?.searchQuery ?? flattenedMenuItems.at(0)?.searchQuery ?? typeMenuSections.at(0)?.menuItems.at(0)?.searchQuery; + + const savedSearchQuery = Object.values(savedSearches ?? {}).at(0)?.query; + const lastQueryFromOnyx = lastSearchParams?.queryJSON ? buildSearchQueryString(lastSearchParams.queryJSON) : undefined; + Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: lastQueryFromOnyx ?? defaultActionableSearchQuery ?? savedSearchQuery ?? buildCannedSearchQuery()})); + }); + }, [selectedTab, typeMenuSections, savedSearches, lastSearchParams?.queryJSON]); + + const navigateToSettings = useCallback(() => { + if (selectedTab === NAVIGATION_TABS.SETTINGS) { + return; + } + interceptAnonymousUser(() => { + const accountTabPayload = getAccountTabScreenToOpen(subscriptionPlan); + + if (isRoutePreloaded(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR)) { + // We use dispatch here because the correct screens and params are preloaded and set up in usePreloadFullScreenNavigators. + navigationRef.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.PUSH, payload: {name: NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR, params: accountTabPayload}}); + return; + } + navigationRef.dispatch(StackActions.push(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR, accountTabPayload)); + }); + }, [selectedTab, subscriptionPlan]); + + /** + * The settings tab is related to SettingsSplitNavigator and WorkspaceSplitNavigator. + * If the user opens this tab from another tab, it is necessary to check whether it has not been opened before. + * If so, all previously opened screens have be pushed to the navigation stack to maintain the order of screens within the tab. + * If the user clicks on the settings tab while on this tab, this button should go back to the previous screen within the tab. + */ + const showWorkspaces = useCallback(() => { + navigateToWorkspacesPage({shouldUseNarrowLayout, currentUserLogin, policy: lastViewedPolicy, domain: lastViewedDomain}); + }, [shouldUseNarrowLayout, currentUserLogin, lastViewedPolicy, lastViewedDomain]); + + const inboxAccessibilityState = useMemo(() => ({selected: selectedTab === NAVIGATION_TABS.INBOX}), [selectedTab]); + const searchAccessibilityState = useMemo(() => ({selected: selectedTab === NAVIGATION_TABS.SEARCH}), [selectedTab]); + const workspacesAccessibilityState = useMemo(() => ({selected: selectedTab === NAVIGATION_TABS.WORKSPACES}), [selectedTab]); + + if (!shouldUseNarrowLayout) { + return ( + <> + {shouldRenderDebugTabViewOnWideLayout && ( + + )} + + + + + + [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]} + sentryLabel="NavigationTabBar.Home" + > + {({hovered}) => ( + <> + + + + + {translate('common.home')} + + + )} + + [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]} + sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.REPORTS} + > + {({hovered}) => ( + <> + + + + + {translate('common.reports')} + + + )} + + [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]} + sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.INBOX} + > + {({hovered}) => ( + <> + + + {!!chatTabBrickRoad && ( + + )} + + + {translate('common.inbox')} + + + )} + + [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]} + sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.WORKSPACES} + > + {({hovered}) => ( + <> + + + {!!workspacesTabIndicatorStatus && ( + + )} + + + {translate('common.workspacesTabTitle')} + + + )} + + + + + + + + + ); + } + + return ( + <> + {!!isDebugModeEnabled && ( + + )} + + + + + + + {translate('common.home')} + + + + + + + + {translate('common.reports')} + + + + + + {!!chatTabBrickRoad && ( + + )} + + + {translate('common.inbox')} + + + + + + {!!workspacesTabIndicatorStatus && } + + + {translate('common.workspacesTabTitle')} + + + + + + + + + {shouldShowFloatingButtons && ( + <> + + + + )} + + ); +} + +export default memo(HomeNavigationTabBar); diff --git a/src/components/Navigation/NavigationTabBar/InboxNavigationTabBar.tsx b/src/components/Navigation/NavigationTabBar/InboxNavigationTabBar.tsx new file mode 100644 index 0000000000000..94c0334d581db --- /dev/null +++ b/src/components/Navigation/NavigationTabBar/InboxNavigationTabBar.tsx @@ -0,0 +1,542 @@ +import {findFocusedRoute, StackActions, useNavigationState} from '@react-navigation/native'; +import reportsSelector from '@selectors/Attributes'; +import React, {memo, useCallback, useEffect, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import type {OnyxCollection} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import FloatingCameraButton from '@components/FloatingCameraButton'; +import FloatingGPSButton from '@components/FloatingGPSButton'; +import Icon from '@components/Icon'; +import ImageSVG from '@components/ImageSVG'; +import DebugTabView from '@components/Navigation/DebugTabView'; +import {PressableWithFeedback} from '@components/Pressable'; +import Text from '@components/Text'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchTypeMenuSections from '@hooks/useSearchTypeMenuSections'; +import {useSidebarOrderedReports} from '@hooks/useSidebarOrderedReports'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useSubscriptionPlan from '@hooks/useSubscriptionPlan'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspacesTabIndicatorStatus from '@hooks/useWorkspacesTabIndicatorStatus'; +import clearSelectedText from '@libs/clearSelectedText/clearSelectedText'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import {getPreservedNavigatorState} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState'; +import getAccountTabScreenToOpen from '@libs/Navigation/helpers/getAccountTabScreenToOpen'; +import isRoutePreloaded from '@libs/Navigation/helpers/isRoutePreloaded'; +import navigateToWorkspacesPage, {getWorkspaceNavigationRouteState} from '@libs/Navigation/helpers/navigateToWorkspacesPage'; +import Navigation from '@libs/Navigation/Navigation'; +import {buildCannedSearchQuery, buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUtils'; +import {getDefaultActionableSearchMenuItem} from '@libs/SearchUIUtils'; +import {startSpan} from '@libs/telemetry/activeSpans'; +import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; +import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils'; +import navigationRef from '@navigation/navigationRef'; +import type {DomainSplitNavigatorParamList, RootNavigatorParamList, SearchFullscreenNavigatorParamList, State, WorkspaceSplitNavigatorParamList} from '@navigation/types'; +import NavigationTabBarAvatar from '@pages/home/sidebar/NavigationTabBarAvatar'; +import NavigationTabBarFloatingActionButton from '@pages/home/sidebar/NavigationTabBarFloatingActionButton'; +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 type {Domain, Policy} from '@src/types/onyx'; +import NAVIGATION_TABS from './NAVIGATION_TABS'; + +type NavigationTabBarProps = { + selectedTab: ValueOf; + isTopLevelBar?: boolean; + shouldShowFloatingButtons?: boolean; +}; + +function InboxNavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatingButtons = true}: NavigationTabBarProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + + const getIconFill = useCallback( + (isSelected: boolean, isHovered: boolean) => { + if (isSelected) { + return theme.iconMenu; + } + if (isHovered) { + return theme.success; + } + return theme.icon; + }, + [theme], + ); + const {translate, preferredLocale} = useLocalize(); + const {indicatorColor: workspacesTabIndicatorColor, status: workspacesTabIndicatorStatus} = useWorkspacesTabIndicatorStatus(); + const {orderedReportIDs} = useSidebarOrderedReports(); + const [isDebugModeEnabled] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED, {canBeMissing: true}); + const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true}); + const navigationState = useNavigationState(findFocusedRoute); + const initialNavigationRouteState = getWorkspaceNavigationRouteState(); + const [lastWorkspacesTabNavigatorRoute, setLastWorkspacesTabNavigatorRoute] = useState(initialNavigationRouteState.lastWorkspacesTabNavigatorRoute); + const [workspacesTabState, setWorkspacesTabState] = useState(initialNavigationRouteState.workspacesTabState); + const params = workspacesTabState?.routes?.at(0)?.params as + | WorkspaceSplitNavigatorParamList[typeof SCREENS.WORKSPACE.INITIAL] + | DomainSplitNavigatorParamList[typeof SCREENS.DOMAIN.INITIAL]; + const {typeMenuSections} = useSearchTypeMenuSections(); + const subscriptionPlan = useSubscriptionPlan(); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['ExpensifyAppIcon', 'Inbox', 'MoneySearch', 'Buildings']); + + const paramsPolicyID = params && 'policyID' in params ? params.policyID : undefined; + const paramsDomainAccountID = params && 'domainAccountID' in params ? params.domainAccountID : undefined; + + const lastViewedPolicySelector = useCallback( + (policies: OnyxCollection) => { + if (!lastWorkspacesTabNavigatorRoute || lastWorkspacesTabNavigatorRoute.name !== NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR || !paramsPolicyID) { + return undefined; + } + + return policies?.[`${ONYXKEYS.COLLECTION.POLICY}${paramsPolicyID}`]; + }, + [paramsPolicyID, lastWorkspacesTabNavigatorRoute], + ); + + const [lastViewedPolicy] = useOnyx( + ONYXKEYS.COLLECTION.POLICY, + { + canBeMissing: true, + selector: lastViewedPolicySelector, + }, + [navigationState], + ); + + const lastViewedDomainSelector = useCallback( + (domains: OnyxCollection) => { + if (!lastWorkspacesTabNavigatorRoute || lastWorkspacesTabNavigatorRoute.name !== NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR || !paramsDomainAccountID) { + return undefined; + } + + return domains?.[`${ONYXKEYS.COLLECTION.DOMAIN}${paramsDomainAccountID}`]; + }, + [paramsDomainAccountID, lastWorkspacesTabNavigatorRoute], + ); + + const [lastViewedDomain] = useOnyx( + ONYXKEYS.COLLECTION.DOMAIN, + { + canBeMissing: true, + selector: lastViewedDomainSelector, + }, + [navigationState], + ); + + const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {selector: reportsSelector, canBeMissing: true}); + const {login: currentUserLogin} = useCurrentUserPersonalDetails(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const [chatTabBrickRoad, setChatTabBrickRoad] = useState(undefined); + + const StyleUtils = useStyleUtils(); + + useEffect(() => { + const newWorkspacesTabState = getWorkspaceNavigationRouteState(); + const newLastRoute = newWorkspacesTabState.lastWorkspacesTabNavigatorRoute; + const newTabState = newWorkspacesTabState.workspacesTabState; + + setLastWorkspacesTabNavigatorRoute(newLastRoute); + setWorkspacesTabState(newTabState); + }, [navigationState]); + + // On a wide layout DebugTabView should be rendered only within the navigation tab bar displayed directly on screens. + const shouldRenderDebugTabViewOnWideLayout = !!isDebugModeEnabled && !isTopLevelBar; + + useEffect(() => { + setChatTabBrickRoad(getChatTabBrickRoad(orderedReportIDs, reportAttributes)); + }, [orderedReportIDs, reportAttributes]); + + const navigateToChats = useCallback(() => { + if (selectedTab === NAVIGATION_TABS.INBOX) { + return; + } + + startSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB, { + name: CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB, + op: CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB, + }); + + if (!shouldUseNarrowLayout && isRoutePreloaded(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR)) { + // We use dispatch here because the correct screens and params are preloaded and set up in usePreloadFullScreenNavigators. + navigationRef.dispatch(StackActions.push(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR)); + return; + } + + Navigation.navigate(ROUTES.INBOX); + }, [selectedTab, shouldUseNarrowLayout]); + + const [lastSearchParams] = useOnyx(ONYXKEYS.REPORT_NAVIGATION_LAST_SEARCH_QUERY, {canBeMissing: true}); + + const navigateToSearch = useCallback(() => { + if (selectedTab === NAVIGATION_TABS.SEARCH) { + return; + } + clearSelectedText(); + interceptAnonymousUser(() => { + const parentSpan = startSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB, { + name: CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB, + op: CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB, + }); + + startSpan(CONST.TELEMETRY.SPAN_ON_LAYOUT_SKELETON_REPORTS, { + name: CONST.TELEMETRY.SPAN_ON_LAYOUT_SKELETON_REPORTS, + op: CONST.TELEMETRY.SPAN_ON_LAYOUT_SKELETON_REPORTS, + parentSpan, + }); + + const rootState = navigationRef.getRootState() as State; + const lastSearchNavigator = rootState.routes.findLast((route) => route.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR); + const lastSearchNavigatorState = lastSearchNavigator && lastSearchNavigator.key ? getPreservedNavigatorState(lastSearchNavigator?.key) : undefined; + const lastSearchRoute = lastSearchNavigatorState?.routes.findLast((route) => route.name === SCREENS.SEARCH.ROOT); + + if (lastSearchRoute) { + const {q, ...rest} = lastSearchRoute.params as SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.ROOT]; + const queryJSON = buildSearchQueryJSON(q); + if (queryJSON) { + const query = buildSearchQueryString(queryJSON); + Navigation.navigate( + ROUTES.SEARCH_ROOT.getRoute({ + query, + ...rest, + }), + ); + return; + } + } + + const flattenedMenuItems = typeMenuSections.flatMap((section) => section.menuItems); + const defaultActionableSearchQuery = + getDefaultActionableSearchMenuItem(flattenedMenuItems)?.searchQuery ?? flattenedMenuItems.at(0)?.searchQuery ?? typeMenuSections.at(0)?.menuItems.at(0)?.searchQuery; + + const savedSearchQuery = Object.values(savedSearches ?? {}).at(0)?.query; + const lastQueryFromOnyx = lastSearchParams?.queryJSON ? buildSearchQueryString(lastSearchParams.queryJSON) : undefined; + Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: lastQueryFromOnyx ?? defaultActionableSearchQuery ?? savedSearchQuery ?? buildCannedSearchQuery()})); + }); + }, [selectedTab, typeMenuSections, savedSearches, lastSearchParams?.queryJSON]); + + const navigateToSettings = useCallback(() => { + if (selectedTab === NAVIGATION_TABS.SETTINGS) { + return; + } + interceptAnonymousUser(() => { + const accountTabPayload = getAccountTabScreenToOpen(subscriptionPlan); + + if (isRoutePreloaded(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR)) { + // We use dispatch here because the correct screens and params are preloaded and set up in usePreloadFullScreenNavigators. + navigationRef.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.PUSH, payload: {name: NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR, params: accountTabPayload}}); + return; + } + navigationRef.dispatch(StackActions.push(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR, accountTabPayload)); + }); + }, [selectedTab, subscriptionPlan]); + + /** + * The settings tab is related to SettingsSplitNavigator and WorkspaceSplitNavigator. + * If the user opens this tab from another tab, it is necessary to check whether it has not been opened before. + * If so, all previously opened screens have be pushed to the navigation stack to maintain the order of screens within the tab. + * If the user clicks on the settings tab while on this tab, this button should go back to the previous screen within the tab. + */ + const showWorkspaces = useCallback(() => { + navigateToWorkspacesPage({shouldUseNarrowLayout, currentUserLogin, policy: lastViewedPolicy, domain: lastViewedDomain}); + }, [shouldUseNarrowLayout, currentUserLogin, lastViewedPolicy, lastViewedDomain]); + + const inboxAccessibilityState = useMemo(() => ({selected: selectedTab === NAVIGATION_TABS.INBOX}), [selectedTab]); + const searchAccessibilityState = useMemo(() => ({selected: selectedTab === NAVIGATION_TABS.SEARCH}), [selectedTab]); + const workspacesAccessibilityState = useMemo(() => ({selected: selectedTab === NAVIGATION_TABS.WORKSPACES}), [selectedTab]); + + if (!shouldUseNarrowLayout) { + return ( + <> + {shouldRenderDebugTabViewOnWideLayout && ( + + )} + + + + + + [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]} + sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.INBOX} + > + {({hovered}) => ( + <> + + + {!!chatTabBrickRoad && ( + + )} + + + {translate('common.inbox')} + + + )} + + [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]} + sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.REPORTS} + > + {({hovered}) => ( + <> + + + + + {translate('common.reports')} + + + )} + + [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]} + sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.WORKSPACES} + > + {({hovered}) => ( + <> + + + {!!workspacesTabIndicatorStatus && ( + + )} + + + {translate('common.workspacesTabTitle')} + + + )} + + + + + + + + + ); + } + + return ( + <> + {!!isDebugModeEnabled && ( + + )} + + + + + {!!chatTabBrickRoad && ( + + )} + + + {translate('common.inbox')} + + + + + + + + {translate('common.reports')} + + + + + + + + + {!!workspacesTabIndicatorStatus && } + + + {translate('common.workspacesTabTitle')} + + + + + {shouldShowFloatingButtons && ( + <> + + + + )} + + ); +} + +export default memo(InboxNavigationTabBar); diff --git a/src/components/Navigation/NavigationTabBar/NAVIGATION_TABS.ts b/src/components/Navigation/NavigationTabBar/NAVIGATION_TABS.ts index 254b6379fa7f6..a870f4400c31a 100644 --- a/src/components/Navigation/NavigationTabBar/NAVIGATION_TABS.ts +++ b/src/components/Navigation/NavigationTabBar/NAVIGATION_TABS.ts @@ -1,5 +1,6 @@ const NAVIGATION_TABS = { HOME: 'HOME', + INBOX: 'INBOX', SEARCH: 'SEARCH', WORKSPACES: 'WORKSPACES', SETTINGS: 'SETTINGS', diff --git a/src/components/Navigation/NavigationTabBar/index.tsx b/src/components/Navigation/NavigationTabBar/index.tsx index 6c53c2b1091aa..39c9d82cc7a81 100644 --- a/src/components/Navigation/NavigationTabBar/index.tsx +++ b/src/components/Navigation/NavigationTabBar/index.tsx @@ -1,53 +1,10 @@ -import {findFocusedRoute, StackActions, useNavigationState} from '@react-navigation/native'; -import reportsSelector from '@selectors/Attributes'; -import React, {memo, useCallback, useEffect, useMemo, useState} from 'react'; -import {View} from 'react-native'; -import type {OnyxCollection} from 'react-native-onyx'; +import React, {memo} from 'react'; import type {ValueOf} from 'type-fest'; -import FloatingCameraButton from '@components/FloatingCameraButton'; -import FloatingGPSButton from '@components/FloatingGPSButton'; -import Icon from '@components/Icon'; -// import * as Expensicons from '@components/Icon/Expensicons'; -import ImageSVG from '@components/ImageSVG'; -import DebugTabView from '@components/Navigation/DebugTabView'; -import {PressableWithFeedback} from '@components/Pressable'; -import Text from '@components/Text'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useSearchTypeMenuSections from '@hooks/useSearchTypeMenuSections'; -import {useSidebarOrderedReports} from '@hooks/useSidebarOrderedReports'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useSubscriptionPlan from '@hooks/useSubscriptionPlan'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWorkspacesTabIndicatorStatus from '@hooks/useWorkspacesTabIndicatorStatus'; -import clearSelectedText from '@libs/clearSelectedText/clearSelectedText'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {getPreservedNavigatorState} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState'; -import getAccountTabScreenToOpen from '@libs/Navigation/helpers/getAccountTabScreenToOpen'; -import isRoutePreloaded from '@libs/Navigation/helpers/isRoutePreloaded'; -import navigateToWorkspacesPage, {getWorkspaceNavigationRouteState} from '@libs/Navigation/helpers/navigateToWorkspacesPage'; -import Navigation from '@libs/Navigation/Navigation'; -import {buildCannedSearchQuery, buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUtils'; -import {getDefaultActionableSearchMenuItem} from '@libs/SearchUIUtils'; -import {startSpan} from '@libs/telemetry/activeSpans'; -import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; -import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils'; -import navigationRef from '@navigation/navigationRef'; -import type {DomainSplitNavigatorParamList, RootNavigatorParamList, SearchFullscreenNavigatorParamList, State, WorkspaceSplitNavigatorParamList} from '@navigation/types'; -import NavigationTabBarAvatar from '@pages/home/sidebar/NavigationTabBarAvatar'; -import NavigationTabBarFloatingActionButton from '@pages/home/sidebar/NavigationTabBarFloatingActionButton'; -import variables from '@styles/variables'; +import usePermissions from '@hooks/usePermissions'; 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 type {Domain, Policy} from '@src/types/onyx'; -import NAVIGATION_TABS from './NAVIGATION_TABS'; +import HomeNavigationTabBar from './HomeNavigationTabBar'; +import InboxNavigationTabBar from './InboxNavigationTabBar'; +import type NAVIGATION_TABS from './NAVIGATION_TABS'; type NavigationTabBarProps = { selectedTab: ValueOf; @@ -56,487 +13,25 @@ type NavigationTabBarProps = { }; function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatingButtons = true}: NavigationTabBarProps) { - const theme = useTheme(); - const styles = useThemeStyles(); + const {isBetaEnabled} = usePermissions(); + const isNewDotHomeEnabled = isBetaEnabled(CONST.BETAS.NEW_DOT_HOME); - const getIconFill = useCallback( - (isSelected: boolean, isHovered: boolean) => { - if (isSelected) { - return theme.iconMenu; - } - if (isHovered) { - return theme.success; - } - return theme.icon; - }, - [theme], - ); - const {translate, preferredLocale} = useLocalize(); - const {indicatorColor: workspacesTabIndicatorColor, status: workspacesTabIndicatorStatus} = useWorkspacesTabIndicatorStatus(); - const {orderedReportIDs} = useSidebarOrderedReports(); - const [isDebugModeEnabled] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED, {canBeMissing: true}); - const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true}); - const navigationState = useNavigationState(findFocusedRoute); - const initialNavigationRouteState = getWorkspaceNavigationRouteState(); - const [lastWorkspacesTabNavigatorRoute, setLastWorkspacesTabNavigatorRoute] = useState(initialNavigationRouteState.lastWorkspacesTabNavigatorRoute); - const [workspacesTabState, setWorkspacesTabState] = useState(initialNavigationRouteState.workspacesTabState); - const params = workspacesTabState?.routes?.at(0)?.params as - | WorkspaceSplitNavigatorParamList[typeof SCREENS.WORKSPACE.INITIAL] - | DomainSplitNavigatorParamList[typeof SCREENS.DOMAIN.INITIAL]; - const {typeMenuSections} = useSearchTypeMenuSections(); - const subscriptionPlan = useSubscriptionPlan(); - const expensifyIcons = useMemoizedLazyExpensifyIcons(['ExpensifyAppIcon', 'Inbox', 'MoneySearch', 'Buildings']); - - const paramsPolicyID = params && 'policyID' in params ? params.policyID : undefined; - const paramsDomainAccountID = params && 'domainAccountID' in params ? params.domainAccountID : undefined; - - const lastViewedPolicySelector = useCallback( - (policies: OnyxCollection) => { - if (!lastWorkspacesTabNavigatorRoute || lastWorkspacesTabNavigatorRoute.name !== NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR || !paramsPolicyID) { - return undefined; - } - - return policies?.[`${ONYXKEYS.COLLECTION.POLICY}${paramsPolicyID}`]; - }, - [paramsPolicyID, lastWorkspacesTabNavigatorRoute], - ); - - const [lastViewedPolicy] = useOnyx( - ONYXKEYS.COLLECTION.POLICY, - { - canBeMissing: true, - selector: lastViewedPolicySelector, - }, - [navigationState], - ); - - const lastViewedDomainSelector = useCallback( - (domains: OnyxCollection) => { - if (!lastWorkspacesTabNavigatorRoute || lastWorkspacesTabNavigatorRoute.name !== NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR || !paramsDomainAccountID) { - return undefined; - } - - return domains?.[`${ONYXKEYS.COLLECTION.DOMAIN}${paramsDomainAccountID}`]; - }, - [paramsDomainAccountID, lastWorkspacesTabNavigatorRoute], - ); - - const [lastViewedDomain] = useOnyx( - ONYXKEYS.COLLECTION.DOMAIN, - { - canBeMissing: true, - selector: lastViewedDomainSelector, - }, - [navigationState], - ); - - const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {selector: reportsSelector, canBeMissing: true}); - const {login: currentUserLogin} = useCurrentUserPersonalDetails(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const [chatTabBrickRoad, setChatTabBrickRoad] = useState(undefined); - - const StyleUtils = useStyleUtils(); - - useEffect(() => { - const newWorkspacesTabState = getWorkspaceNavigationRouteState(); - const newLastRoute = newWorkspacesTabState.lastWorkspacesTabNavigatorRoute; - const newTabState = newWorkspacesTabState.workspacesTabState; - - setLastWorkspacesTabNavigatorRoute(newLastRoute); - setWorkspacesTabState(newTabState); - }, [navigationState]); - - // On a wide layout DebugTabView should be rendered only within the navigation tab bar displayed directly on screens. - const shouldRenderDebugTabViewOnWideLayout = !!isDebugModeEnabled && !isTopLevelBar; - - useEffect(() => { - setChatTabBrickRoad(getChatTabBrickRoad(orderedReportIDs, reportAttributes)); - }, [orderedReportIDs, reportAttributes]); - - const navigateToChats = useCallback(() => { - if (selectedTab === NAVIGATION_TABS.HOME) { - return; - } - - startSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB, { - name: CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB, - op: CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB, - }); - - if (!shouldUseNarrowLayout && isRoutePreloaded(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR)) { - // We use dispatch here because the correct screens and params are preloaded and set up in usePreloadFullScreenNavigators. - navigationRef.dispatch(StackActions.push(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR)); - return; - } - - Navigation.navigate(ROUTES.HOME); - }, [selectedTab, shouldUseNarrowLayout]); - - const [lastSearchParams] = useOnyx(ONYXKEYS.REPORT_NAVIGATION_LAST_SEARCH_QUERY, {canBeMissing: true}); - - const navigateToSearch = useCallback(() => { - if (selectedTab === NAVIGATION_TABS.SEARCH) { - return; - } - clearSelectedText(); - interceptAnonymousUser(() => { - const parentSpan = startSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB, { - name: CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB, - op: CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB, - }); - - startSpan(CONST.TELEMETRY.SPAN_ON_LAYOUT_SKELETON_REPORTS, { - name: CONST.TELEMETRY.SPAN_ON_LAYOUT_SKELETON_REPORTS, - op: CONST.TELEMETRY.SPAN_ON_LAYOUT_SKELETON_REPORTS, - parentSpan, - }); - - const rootState = navigationRef.getRootState() as State; - const lastSearchNavigator = rootState.routes.findLast((route) => route.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR); - const lastSearchNavigatorState = lastSearchNavigator && lastSearchNavigator.key ? getPreservedNavigatorState(lastSearchNavigator?.key) : undefined; - const lastSearchRoute = lastSearchNavigatorState?.routes.findLast((route) => route.name === SCREENS.SEARCH.ROOT); - - if (lastSearchRoute) { - const {q, ...rest} = lastSearchRoute.params as SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.ROOT]; - const queryJSON = buildSearchQueryJSON(q); - if (queryJSON) { - const query = buildSearchQueryString(queryJSON); - Navigation.navigate( - ROUTES.SEARCH_ROOT.getRoute({ - query, - ...rest, - }), - ); - return; - } - } - - const flattenedMenuItems = typeMenuSections.flatMap((section) => section.menuItems); - const defaultActionableSearchQuery = - getDefaultActionableSearchMenuItem(flattenedMenuItems)?.searchQuery ?? flattenedMenuItems.at(0)?.searchQuery ?? typeMenuSections.at(0)?.menuItems.at(0)?.searchQuery; - - const savedSearchQuery = Object.values(savedSearches ?? {}).at(0)?.query; - const lastQueryFromOnyx = lastSearchParams?.queryJSON ? buildSearchQueryString(lastSearchParams.queryJSON) : undefined; - Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: lastQueryFromOnyx ?? defaultActionableSearchQuery ?? savedSearchQuery ?? buildCannedSearchQuery()})); - }); - }, [selectedTab, typeMenuSections, savedSearches, lastSearchParams?.queryJSON]); - - const navigateToSettings = useCallback(() => { - if (selectedTab === NAVIGATION_TABS.SETTINGS) { - return; - } - interceptAnonymousUser(() => { - const accountTabPayload = getAccountTabScreenToOpen(subscriptionPlan); - - if (isRoutePreloaded(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR)) { - // We use dispatch here because the correct screens and params are preloaded and set up in usePreloadFullScreenNavigators. - navigationRef.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.PUSH, payload: {name: NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR, params: accountTabPayload}}); - return; - } - navigationRef.dispatch(StackActions.push(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR, accountTabPayload)); - }); - }, [selectedTab, subscriptionPlan]); - - /** - * The settings tab is related to SettingsSplitNavigator and WorkspaceSplitNavigator. - * If the user opens this tab from another tab, it is necessary to check whether it has not been opened before. - * If so, all previously opened screens have be pushed to the navigation stack to maintain the order of screens within the tab. - * If the user clicks on the settings tab while on this tab, this button should go back to the previous screen within the tab. - */ - const showWorkspaces = useCallback(() => { - navigateToWorkspacesPage({shouldUseNarrowLayout, currentUserLogin, policy: lastViewedPolicy, domain: lastViewedDomain}); - }, [shouldUseNarrowLayout, currentUserLogin, lastViewedPolicy, lastViewedDomain]); - - const inboxAccessibilityState = useMemo(() => ({selected: selectedTab === NAVIGATION_TABS.HOME}), [selectedTab]); - const searchAccessibilityState = useMemo(() => ({selected: selectedTab === NAVIGATION_TABS.SEARCH}), [selectedTab]); - const workspacesAccessibilityState = useMemo(() => ({selected: selectedTab === NAVIGATION_TABS.WORKSPACES}), [selectedTab]); - - if (!shouldUseNarrowLayout) { + if (isNewDotHomeEnabled) { return ( - <> - {shouldRenderDebugTabViewOnWideLayout && ( - - )} - - - - - - [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]} - sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.INBOX} - > - {({hovered}) => ( - <> - - - {!!chatTabBrickRoad && ( - - )} - - - {translate('common.inbox')} - - - )} - - [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]} - sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.REPORTS} - > - {({hovered}) => ( - <> - - - - - {translate('common.reports')} - - - )} - - [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]} - sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.WORKSPACES} - > - {({hovered}) => ( - <> - - - {!!workspacesTabIndicatorStatus && ( - - )} - - - {translate('common.workspacesTabTitle')} - - - )} - - - - - - - - + ); } return ( - <> - {!!isDebugModeEnabled && ( - - )} - - - - - {!!chatTabBrickRoad && ( - - )} - - - {translate('common.inbox')} - - - - - - - - {translate('common.reports')} - - - - - - - - - {!!workspacesTabIndicatorStatus && } - - - {translate('common.workspacesTabTitle')} - - - - - {shouldShowFloatingButtons && ( - <> - - - - )} - + ); } diff --git a/src/components/Navigation/TopLevelNavigationTabBar/SCREENS_WITH_NAVIGATION_TAB_BAR.ts b/src/components/Navigation/TopLevelNavigationTabBar/SCREENS_WITH_NAVIGATION_TAB_BAR.ts index c5fd98bac153e..5976defa8e191 100644 --- a/src/components/Navigation/TopLevelNavigationTabBar/SCREENS_WITH_NAVIGATION_TAB_BAR.ts +++ b/src/components/Navigation/TopLevelNavigationTabBar/SCREENS_WITH_NAVIGATION_TAB_BAR.ts @@ -2,6 +2,6 @@ import {SIDEBAR_TO_SPLIT} from '@libs/Navigation/linkingConfig/RELATIONS'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; -const SCREENS_WITH_NAVIGATION_TAB_BAR = [...Object.keys(SIDEBAR_TO_SPLIT), NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR, SCREENS.SEARCH.ROOT, SCREENS.WORKSPACES_LIST]; +const SCREENS_WITH_NAVIGATION_TAB_BAR = [...Object.keys(SIDEBAR_TO_SPLIT), NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR, SCREENS.SEARCH.ROOT, SCREENS.WORKSPACES_LIST, SCREENS.HOME]; export default SCREENS_WITH_NAVIGATION_TAB_BAR; diff --git a/src/components/PriorityModeController.tsx b/src/components/PriorityModeController.tsx index d1912aa0d325d..a29364ee48be4 100644 --- a/src/components/PriorityModeController.tsx +++ b/src/components/PriorityModeController.tsx @@ -77,7 +77,7 @@ export default function PriorityModeController() { // We wait for the user to navigate back to the home screen before triggering this switch const isNarrowLayout = getIsNarrowLayout(); - if ((isNarrowLayout && currentRouteName !== SCREENS.HOME) || (!isNarrowLayout && currentRouteName !== SCREENS.REPORT)) { + if ((isNarrowLayout && currentRouteName !== SCREENS.INBOX) || (!isNarrowLayout && currentRouteName !== SCREENS.REPORT)) { Log.info("[PriorityModeController] Not switching user to focus mode as they aren't on the home screen", false, {validReportCount, currentRouteName}); return; } diff --git a/src/components/ScrollOffsetContextProvider.tsx b/src/components/ScrollOffsetContextProvider.tsx index 1c3fbc9052f83..8a170ed26430a 100644 --- a/src/components/ScrollOffsetContextProvider.tsx +++ b/src/components/ScrollOffsetContextProvider.tsx @@ -71,7 +71,7 @@ function ScrollOffsetContextProvider({children}: ScrollOffsetContextProviderProp // If the priority mode changes, we need to clear the scroll offsets for the home and search screens because it affects the size of the elements and scroll positions wouldn't be correct. for (const key of Object.keys(scrollOffsetsRef.current)) { - if (key.includes(SCREENS.HOME) || key.includes(SCREENS.SEARCH.ROOT)) { + if (key.includes(SCREENS.INBOX) || key.includes(SCREENS.SEARCH.ROOT)) { delete scrollOffsetsRef.current[key]; } } diff --git a/src/components/Search/SearchRouter/SearchRouterPage/index.tsx b/src/components/Search/SearchRouter/SearchRouterPage/index.tsx index ca82a5518ac6e..4f6ec24c7d48d 100644 --- a/src/components/Search/SearchRouter/SearchRouterPage/index.tsx +++ b/src/components/Search/SearchRouter/SearchRouterPage/index.tsx @@ -5,7 +5,8 @@ import ROUTES from '@src/ROUTES'; * On native devices SearchRouter is served from SearchRouterPage, on web from SearchRouterModal. */ function SearchRouterPage() { - return Navigation.navigate(ROUTES.HOME); + // @TODO: Navigate to HOME when removing the newDotHome beta + return Navigation.navigate(ROUTES.INBOX); } export default SearchRouterPage; diff --git a/src/components/TestToolsModalPage.tsx b/src/components/TestToolsModalPage.tsx index f1a5635608652..661d0992ee192 100644 --- a/src/components/TestToolsModalPage.tsx +++ b/src/components/TestToolsModalPage.tsx @@ -38,7 +38,7 @@ function TestToolsModalPage() { // If no backTo param is provided (direct access to /test-tools), // use home route as a default backTo param for console navigation - const effectiveBackTo = backTo ?? ROUTES.HOME; + const effectiveBackTo = backTo ?? ROUTES.INBOX; const consoleRoute = getRouteBasedOnAuthStatus(isAuthenticated, effectiveBackTo); const maxHeight = windowHeight; diff --git a/src/languages/de.ts b/src/languages/de.ts index 6c88f1680c3a5..dce355cf8eca3 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -638,6 +638,7 @@ const translations: TranslationDeepObject = { duplicateExpense: 'Doppelte Ausgabe', newFeature: 'Neue Funktion', month: 'Monat', + home: 'Startseite', }, supportalNoAccess: { title: 'Nicht so schnell', diff --git a/src/languages/en.ts b/src/languages/en.ts index cbb3210a55181..574326d75ff10 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -273,6 +273,7 @@ const translations = { digits: 'digits', twoFactorCode: 'Two-factor code', workspaces: 'Workspaces', + home: 'Home', inbox: 'Inbox', // @context Used in confirmation or result messages indicating that an action completed successfully, not the abstract noun “success.” success: 'Success', diff --git a/src/languages/es.ts b/src/languages/es.ts index 39b7468eb2514..fdfc75d52e6d4 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -55,6 +55,7 @@ const translations: TranslationDeepObject = { twoFactorCode: 'Autenticación de dos factores', workspaces: 'Espacios de trabajo', inbox: 'Recibidos', + home: 'Inicio', group: 'Grupo', profile: 'Perfil', referral: 'Remisión', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 7f08b0533309c..6b7a40980f9bf 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -640,6 +640,7 @@ const translations: TranslationDeepObject = { duplicateExpense: 'Note de frais en double', newFeature: 'Nouvelle fonctionnalité', month: 'Mois', + home: 'Accueil', }, supportalNoAccess: { title: 'Pas si vite', diff --git a/src/languages/it.ts b/src/languages/it.ts index 7525885d06801..e260917992fdc 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -639,6 +639,7 @@ const translations: TranslationDeepObject = { duplicateExpense: 'Spesa duplicata', newFeature: 'Nuova funzionalità', month: 'Mese', + home: 'Home', }, supportalNoAccess: { title: 'Non così in fretta', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 43f10ab5574ea..9713a504654f8 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -638,6 +638,7 @@ const translations: TranslationDeepObject = { duplicateExpense: '重複した経費', newFeature: '新機能', month: '月', + home: 'ホーム', }, supportalNoAccess: { title: 'ちょっと待ってください', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 5289966a87525..11fad6ac84d80 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -639,6 +639,7 @@ const translations: TranslationDeepObject = { duplicateExpense: 'Dubbele uitgave', newFeature: 'Nieuwe functie', month: 'Maand', + home: 'Start', }, supportalNoAccess: { title: 'Niet zo snel', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index a37726567c98c..6f4cd6d4d71cb 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -639,6 +639,7 @@ const translations: TranslationDeepObject = { duplicateExpense: 'Zduplikowany wydatek', newFeature: 'Nowa funkcja', month: 'Miesiąc', + home: 'Strona główna', }, supportalNoAccess: { title: 'Nie tak szybko', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index ee7cd0e6e3365..07687f1951424 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -638,6 +638,7 @@ const translations: TranslationDeepObject = { duplicateExpense: 'Despesa duplicada', newFeature: 'Novo recurso', month: 'Mês', + home: 'Início', }, supportalNoAccess: { title: 'Não tão rápido', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index ee322f52c2c71..bae60f19fe07e 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -635,6 +635,7 @@ const translations: TranslationDeepObject = { duplicateExpense: '重复报销', newFeature: '新功能', month: '月', + home: '首页', }, supportalNoAccess: { title: '先别急', diff --git a/src/libs/LoginUtils.ts b/src/libs/LoginUtils.ts index 6201df99d63ff..d9de9141cb500 100644 --- a/src/libs/LoginUtils.ts +++ b/src/libs/LoginUtils.ts @@ -105,7 +105,7 @@ function handleSAMLLoginError(errorMessage: string, shouldClearSignInData: boole } setAccountError(errorMessage); - Navigation.goBack(ROUTES.HOME); + Navigation.goBack(ROUTES.INBOX); } function formatE164PhoneNumber(phoneNumber: string, countryCode: number) { diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 19d6672ae2eef..1afe91bbd0a21 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -86,6 +86,7 @@ const loadLogOutPreviousUserPage = () => require('../../.. const loadConciergePage = () => require('../../../pages/ConciergePage').default; const loadTrackExpensePage = () => require('../../../pages/TrackExpensePage').default; const loadSubmitExpensePage = () => require('../../../pages/SubmitExpensePage').default; +const loadHomePage = () => require('../../../pages/HomePage').default; const loadWorkspaceJoinUser = () => require('@pages/workspace/WorkspaceJoinUserPage').default; const loadReportSplitNavigator = () => require('./Navigators/ReportsSplitNavigator').default; @@ -524,6 +525,7 @@ function AuthScreens() { NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR, NAVIGATORS.RIGHT_MODAL_NAVIGATOR, SCREENS.WORKSPACES_LIST, + SCREENS.HOME, SCREENS.SEARCH.ROOT, ]} > @@ -533,6 +535,11 @@ function AuthScreens() { options={getFullscreenNavigatorOptions} getComponent={loadReportSplitNavigator} /> + diff --git a/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts b/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts index 49a9448d2f122..0fb4d2556ea68 100644 --- a/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts +++ b/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts @@ -25,7 +25,7 @@ import {getPreservedNavigatorState} from './createSplitNavigator/usePreserveNavi const TIMING_TO_CALL_PRELOAD = 1000; // Currently the Inbox, Workspaces and Account tabs are preloaded, while Search is not preloaded due to its potential complexity. -const TABS_TO_PRELOAD = [NAVIGATION_TABS.HOME, NAVIGATION_TABS.WORKSPACES, NAVIGATION_TABS.SETTINGS]; +const TABS_TO_PRELOAD = [NAVIGATION_TABS.INBOX, NAVIGATION_TABS.WORKSPACES, NAVIGATION_TABS.SETTINGS]; function preloadWorkspacesTab(navigation: PlatformStackNavigationProp) { const state = getWorkspacesTabStateFromSessionStorage() ?? navigation.getState(); @@ -61,7 +61,11 @@ function preloadAccountTab(navigation: PlatformStackNavigationProp) { - navigation.preload(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, {screen: SCREENS.HOME}); + navigation.preload(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, {screen: SCREENS.INBOX}); +} + +function preloadHomeTab(navigation: PlatformStackNavigationProp) { + navigation.preload(SCREENS.HOME); } function preloadTab(tabName: string, navigation: PlatformStackNavigationProp, subscriptionPlan: ValueOf | null) { @@ -75,9 +79,12 @@ function preloadTab(tabName: string, navigation: PlatformStackNavigationProp { cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator({props, isFullScreenModal: true}), }, }, - workspacesListPage: { + fullScreenTabPage: { ...commonScreenOptions, // We need to turn off animation for the full screen to avoid delay when closing screens. animation: Animations.NONE, diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 632b3c49e0fa2..47f38ca4df3c6 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -505,7 +505,7 @@ function resetToHome() { name: SCREENS.REPORT, } : undefined; - const payload = getInitialSplitNavigatorState({name: SCREENS.HOME}, splitNavigatorMainScreen); + const payload = getInitialSplitNavigatorState({name: SCREENS.INBOX}, splitNavigatorMainScreen); navigationRef.dispatch({payload, type: CONST.NAVIGATION.ACTION_TYPE.REPLACE, target: rootState.key}); } @@ -519,7 +519,7 @@ function goBackToHome() { const isNarrowLayout = getIsNarrowLayout(); // This set the right split navigator. - goBack(ROUTES.HOME); + goBack(ROUTES.INBOX); // We want to keep the report screen in the split navigator on wide layout. if (!isNarrowLayout) { @@ -527,7 +527,7 @@ function goBackToHome() { } // This set the right route in this split navigator. - goBack(ROUTES.HOME); + goBack(ROUTES.INBOX); } /** diff --git a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts index af421e67df2a8..c75a5b6321c14 100644 --- a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts @@ -180,7 +180,7 @@ function getDefaultFullScreenRoute(route?: NavigationPartialRoute) { return getInitialSplitNavigatorState( { - name: SCREENS.HOME, + name: SCREENS.INBOX, }, { name: SCREENS.REPORT, diff --git a/src/libs/Navigation/helpers/isNavigatorName.ts b/src/libs/Navigation/helpers/isNavigatorName.ts index 4fdebfb9bdf7e..959bef85c5b04 100644 --- a/src/libs/Navigation/helpers/isNavigatorName.ts +++ b/src/libs/Navigation/helpers/isNavigatorName.ts @@ -3,7 +3,7 @@ import type {FullScreenName, OnboardingFlowName, SplitNavigatorName, SplitNaviga import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; -const FULL_SCREENS_SET = new Set([...Object.values(SIDEBAR_TO_SPLIT), NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR, SCREENS.WORKSPACES_LIST]); +const FULL_SCREENS_SET = new Set([...Object.values(SIDEBAR_TO_SPLIT), NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR, SCREENS.WORKSPACES_LIST, SCREENS.HOME]); const SIDEBARS_SET = new Set(Object.values(SPLIT_TO_SIDEBAR)); const ONBOARDING_SCREENS_SET = new Set(Object.values(SCREENS.ONBOARDING)); const SPLIT_NAVIGATORS_SET = new Set(Object.values(SIDEBAR_TO_SPLIT)); diff --git a/src/libs/Navigation/helpers/useIsHomeRouteActive.ts b/src/libs/Navigation/helpers/useIsHomeRouteActive.ts index 9c7c3fb63b5c0..186f1c92fe8ac 100644 --- a/src/libs/Navigation/helpers/useIsHomeRouteActive.ts +++ b/src/libs/Navigation/helpers/useIsHomeRouteActive.ts @@ -11,7 +11,7 @@ function useIsHomeRouteActive(isNarrowLayout: boolean) { const navigationState = useRootNavigationState((x) => x); if (isNarrowLayout) { - return focusedRoute?.name === SCREENS.HOME; + return focusedRoute?.name === SCREENS.INBOX; } // On full width screens HOME is always a sidebar to the Reports Screen diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_SPLIT.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_SPLIT.ts index 47f9ada982818..5022e2a5da1f9 100644 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_SPLIT.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_SPLIT.ts @@ -4,7 +4,7 @@ import SCREENS from '@src/SCREENS'; // This file is used to define the relationship between the sidebar (LHN) and the parent split navigator. const SIDEBAR_TO_SPLIT = { [SCREENS.SETTINGS.ROOT]: NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR, - [SCREENS.HOME]: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, + [SCREENS.INBOX]: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, [SCREENS.WORKSPACE.INITIAL]: NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, [SCREENS.DOMAIN.INITIAL]: NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR, }; diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/TAB_TO_FULLSCREEN.ts b/src/libs/Navigation/linkingConfig/RELATIONS/TAB_TO_FULLSCREEN.ts index 3fd187d043e28..e41740a1ad0b7 100644 --- a/src/libs/Navigation/linkingConfig/RELATIONS/TAB_TO_FULLSCREEN.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/TAB_TO_FULLSCREEN.ts @@ -5,7 +5,8 @@ import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; const TAB_TO_FULLSCREEN: Record, FullScreenName[]> = { - [NAVIGATION_TABS.HOME]: [NAVIGATORS.REPORTS_SPLIT_NAVIGATOR], + [NAVIGATION_TABS.HOME]: [SCREENS.HOME], + [NAVIGATION_TABS.INBOX]: [NAVIGATORS.REPORTS_SPLIT_NAVIGATOR], [NAVIGATION_TABS.SEARCH]: [NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR], [NAVIGATION_TABS.SETTINGS]: [NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR], [NAVIGATION_TABS.WORKSPACES]: [SCREENS.WORKSPACES_LIST, NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR], diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 1ec566d5d10ed..7ff27f950e168 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -20,6 +20,7 @@ const config: LinkingOptions['config'] = { [SCREENS.CONCIERGE]: ROUTES.CONCIERGE, [SCREENS.TRACK_EXPENSE]: ROUTES.TRACK_EXPENSE, [SCREENS.SUBMIT_EXPENSE]: ROUTES.SUBMIT_EXPENSE, + [SCREENS.HOME]: ROUTES.HOME, [SCREENS.SAML_SIGN_IN]: ROUTES.SAML_SIGN_IN, [SCREENS.REPORT_ATTACHMENTS]: ROUTES.REPORT_ATTACHMENTS.route, [SCREENS.REPORT_ADD_ATTACHMENT]: ROUTES.REPORT_ADD_ATTACHMENT.route, @@ -1962,8 +1963,8 @@ const config: LinkingOptions['config'] = { [NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]: { path: ROUTES.ROOT, screens: { - [SCREENS.HOME]: { - path: ROUTES.HOME, + [SCREENS.INBOX]: { + path: ROUTES.INBOX, exact: true, }, [SCREENS.REPORT]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 1cee192f433b9..268a76fc9f63f 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2413,7 +2413,7 @@ type TravelNavigatorParamList = { }; type ReportsSplitNavigatorParamList = { - [SCREENS.HOME]: undefined; + [SCREENS.INBOX]: undefined; [SCREENS.REPORT]: { reportID: string; reportActionID?: string; @@ -2812,6 +2812,7 @@ type AuthScreensParamList = SharedScreensParamList & [SCREENS.CONCIERGE]: undefined; [SCREENS.TRACK_EXPENSE]: undefined; [SCREENS.SUBMIT_EXPENSE]: undefined; + [SCREENS.HOME]: undefined; [SCREENS.WORKSPACES_LIST]: { // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md backTo?: Routes; @@ -3021,7 +3022,7 @@ type SplitNavigatorName = keyof SplitNavigatorParamList; type SearchFullscreenNavigatorName = typeof NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR; -type FullScreenName = SplitNavigatorName | SearchFullscreenNavigatorName | typeof SCREENS.WORKSPACES_LIST; +type FullScreenName = SplitNavigatorName | SearchFullscreenNavigatorName | typeof SCREENS.WORKSPACES_LIST | typeof SCREENS.HOME; // There are three screens/navigators which can be displayed when the Workspaces tab is selected type WorkspacesTabNavigatorName = typeof SCREENS.WORKSPACES_LIST | typeof NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR | typeof NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR; diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 71685a8b74339..23086c1a38b17 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -686,7 +686,7 @@ function clearOnyxAndResetApp(shouldNavigateToHomepage?: boolean) { } if (shouldNavigateToHomepage) { - Navigation.navigate(ROUTES.HOME); + Navigation.navigate(ROUTES.INBOX); } if (preservedUserSession) { diff --git a/src/pages/ConciergePage.tsx b/src/pages/ConciergePage.tsx index 5bdd29afeb1ff..2c0bbf9dece7c 100644 --- a/src/pages/ConciergePage.tsx +++ b/src/pages/ConciergePage.tsx @@ -36,7 +36,7 @@ function ConciergePage() { navigateToConciergeChat(conciergeReportID, true, () => !isUnmounted.current); }); } else { - Navigation.navigate(ROUTES.HOME); + Navigation.navigate(ROUTES.INBOX); } }, [session, isLoadingReportData, conciergeReportID]), ); diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx new file mode 100644 index 0000000000000..f1d5ea50ffd1b --- /dev/null +++ b/src/pages/HomePage.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import NavigationTabBar from '@components/Navigation/NavigationTabBar'; +import NAVIGATION_TABS from '@components/Navigation/NavigationTabBar/NAVIGATION_TABS'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; + +function HomePage() { + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const shouldDisplayLHB = !shouldUseNarrowLayout; + + return ( + + ) + } + > + {shouldDisplayLHB && } + + ); +} + +export default HomePage; diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.tsx b/src/pages/LogInWithShortLivedAuthTokenPage.tsx index fb3c9224abe68..2289dd4e1fb23 100644 --- a/src/pages/LogInWithShortLivedAuthTokenPage.tsx +++ b/src/pages/LogInWithShortLivedAuthTokenPage.tsx @@ -34,7 +34,7 @@ function LogInWithShortLivedAuthTokenPage({route}: LogInWithShortLivedAuthTokenP Navigation.isNavigationReady().then(() => { // We must call goBack() to remove the /transition route from history Navigation.goBack(); - Navigation.navigate(ROUTES.HOME); + Navigation.navigate(ROUTES.INBOX); }); return; } diff --git a/src/pages/LogOutPreviousUserPage.tsx b/src/pages/LogOutPreviousUserPage.tsx index 4b4b56d1e26c0..fa5342042debc 100644 --- a/src/pages/LogOutPreviousUserPage.tsx +++ b/src/pages/LogOutPreviousUserPage.tsx @@ -44,7 +44,7 @@ function LogOutPreviousUserPage({route}: LogOutPreviousUserPageProps) { Navigation.isNavigationReady().then(() => { // We must call goBack() to remove the /transition route from history Navigation.goBack(); - Navigation.navigate(ROUTES.HOME); + Navigation.navigate(ROUTES.INBOX); }); return; } diff --git a/src/pages/Share/ShareRootPage.tsx b/src/pages/Share/ShareRootPage.tsx index 3f271b130b4c0..f963fac53787c 100644 --- a/src/pages/Share/ShareRootPage.tsx +++ b/src/pages/Share/ShareRootPage.tsx @@ -29,11 +29,11 @@ function showErrorAlert(title: string, message: string) { Alert.alert(title, message, [ { onPress: () => { - Navigation.navigate(ROUTES.HOME); + Navigation.navigate(ROUTES.INBOX); }, }, ]); - Navigation.navigate(ROUTES.HOME); + Navigation.navigate(ROUTES.INBOX); } function ShareRootPage() { @@ -180,7 +180,7 @@ function ShareRootPage() { Navigation.navigate(ROUTES.HOME)} + onBackButtonPress={() => Navigation.navigate(ROUTES.INBOX)} /> {isFileReady ? ( } + bottomContent={!shouldDisplayLHB && } > {({insets}) => ( <> @@ -39,7 +39,7 @@ function BaseSidebarScreen() { - {shouldDisplayLHB && } + {shouldDisplayLHB && } )} diff --git a/src/pages/home/sidebar/SidebarLinks.tsx b/src/pages/home/sidebar/SidebarLinks.tsx index 18dad286de5c0..ac0afa5984ba0 100644 --- a/src/pages/home/sidebar/SidebarLinks.tsx +++ b/src/pages/home/sidebar/SidebarLinks.tsx @@ -60,7 +60,7 @@ function SidebarLinks({insets, optionListItems, isLoading, priorityMode = CONST. // Prevent opening a new Report page if the user quickly taps on another conversation // before the first one is displayed. - const shouldBlockReportNavigation = Navigation.getActiveRoute() !== '/home' && shouldUseNarrowLayout; + const shouldBlockReportNavigation = Navigation.getActiveRoute() !== `/${ROUTES.INBOX}` && shouldUseNarrowLayout; if ( (option.reportID === Navigation.getTopmostReportId() && !reportActionID) || diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 676e081eec7e8..4da2e4d5a552b 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -338,7 +338,7 @@ function IOURequestStepScan({ if (backTo) { Navigation.navigate(backTo as Route); } else { - Navigation.navigate(ROUTES.HOME); + Navigation.navigate(ROUTES.INBOX); } } else { navigateBack(); diff --git a/src/pages/media/AttachmentModalScreen/routes/hooks/useNavigateToReportOnRefresh.ts b/src/pages/media/AttachmentModalScreen/routes/hooks/useNavigateToReportOnRefresh.ts index 66928ecfa2a20..03e81f90a7378 100644 --- a/src/pages/media/AttachmentModalScreen/routes/hooks/useNavigateToReportOnRefresh.ts +++ b/src/pages/media/AttachmentModalScreen/routes/hooks/useNavigateToReportOnRefresh.ts @@ -21,7 +21,7 @@ function useNavigateToReportOnRefresh({source, file, reportID}: UseReportNavigat if (reportID) { Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(reportID)); } else { - Navigation.goBack(ROUTES.HOME); + Navigation.goBack(ROUTES.INBOX); } }); }, [source, reportID, file]); diff --git a/src/pages/signin/SAMLSignInPage/index.native.tsx b/src/pages/signin/SAMLSignInPage/index.native.tsx index e17a3cdd645d7..b4732e30c3f70 100644 --- a/src/pages/signin/SAMLSignInPage/index.native.tsx +++ b/src/pages/signin/SAMLSignInPage/index.native.tsx @@ -79,7 +79,7 @@ function SAMLSignInPage() { Navigation.isNavigationReady().then(() => { // We must call goBack() to remove the /transition route from history Navigation.goBack(); - Navigation.navigate(ROUTES.HOME); + Navigation.navigate(ROUTES.INBOX); }); } }, diff --git a/src/styles/index.ts b/src/styles/index.ts index 044d9b93c1045..d27ec88e12992 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1665,6 +1665,22 @@ const staticStyles = (theme: ThemeColors) => zIndex: 10, }, + floatingCameraButtonAboveFab: { + position: 'absolute', + // floatingActionButton top property value (componentSizeLarge + 16) + + // + floatingCameraButton height (componentSizeLarge) + gap (12) = 2 * componentSizeLarge + 28 + top: 2 * -variables.componentSizeLarge - 28, + right: 16, + zIndex: 10, + }, + + floatingActionButtonPosition: { + position: 'absolute', + top: -variables.componentSizeLarge - 16, + right: 16, + zIndex: 10, + }, + floatingGpsButton: { position: 'absolute', // floatingCameraButton top property value (componentSizeLarge + 16) + @@ -1674,6 +1690,15 @@ const staticStyles = (theme: ThemeColors) => zIndex: 10, }, + floatingGpsButtonAboveFab: { + position: 'absolute', + // floatingActionButton top property value (componentSizeLarge + 16) + + // + floatingCameraButton height (componentSizeLarge) + gap (12) + floatingGpsButton height (componentSizeLarge) + gap (12) = 3 * variables.componentSizeLarge + 40 + top: 3 * -variables.componentSizeLarge - 40, + right: 16, + zIndex: 10, + }, + topBarLabel: { color: theme.text, fontSize: variables.fontSizeXLarge, diff --git a/src/styles/theme/themes/dark.ts b/src/styles/theme/themes/dark.ts index ef5734bab4568..655668694bc20 100644 --- a/src/styles/theme/themes/dark.ts +++ b/src/styles/theme/themes/dark.ts @@ -116,6 +116,10 @@ const darkTheme = { backgroundColor: colors.productDark100, statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT, }, + [SCREENS.INBOX]: { + backgroundColor: colors.productDark100, + statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT, + }, [SCREENS.REPORT]: { backgroundColor: colors.productDark100, statusBarStyle: CONST.STATUS_BAR_STYLE.LIGHT_CONTENT, diff --git a/src/styles/theme/themes/light.ts b/src/styles/theme/themes/light.ts index 1a21846692a53..f344ad0e18bb4 100644 --- a/src/styles/theme/themes/light.ts +++ b/src/styles/theme/themes/light.ts @@ -116,6 +116,10 @@ const lightTheme = { backgroundColor: colors.productLight100, statusBarStyle: CONST.STATUS_BAR_STYLE.DARK_CONTENT, }, + [SCREENS.INBOX]: { + backgroundColor: colors.productLight100, + statusBarStyle: CONST.STATUS_BAR_STYLE.DARK_CONTENT, + }, [SCREENS.REPORT]: { backgroundColor: colors.productLight100, statusBarStyle: CONST.STATUS_BAR_STYLE.DARK_CONTENT, diff --git a/tests/navigation/GoBackTests.tsx b/tests/navigation/GoBackTests.tsx index 9a7cc81787a1c..b0e25fd102fbe 100644 --- a/tests/navigation/GoBackTests.tsx +++ b/tests/navigation/GoBackTests.tsx @@ -185,7 +185,7 @@ describe('Go back on the narrow layout', () => { index: 2, routes: [ { - name: SCREENS.HOME, + name: SCREENS.INBOX, }, { name: SCREENS.REPORT, @@ -248,7 +248,7 @@ describe('Go back on the narrow layout', () => { index: 2, routes: [ { - name: SCREENS.HOME, + name: SCREENS.INBOX, }, { name: SCREENS.REPORT, @@ -307,7 +307,7 @@ describe('Go back on the narrow layout', () => { index: 3, routes: [ { - name: SCREENS.HOME, + name: SCREENS.INBOX, }, { name: SCREENS.REPORT, @@ -359,7 +359,7 @@ describe('Go back on the narrow layout', () => { index: 2, routes: [ { - name: SCREENS.HOME, + name: SCREENS.INBOX, }, { name: SCREENS.REPORT, @@ -407,7 +407,7 @@ describe('Go back on the narrow layout', () => { index: 3, routes: [ { - name: SCREENS.HOME, + name: SCREENS.INBOX, }, { name: SCREENS.REPORT, diff --git a/tests/navigation/NavigateTests.tsx b/tests/navigation/NavigateTests.tsx index 28825c199ff4b..d8ee89714c07c 100644 --- a/tests/navigation/NavigateTests.tsx +++ b/tests/navigation/NavigateTests.tsx @@ -159,7 +159,7 @@ describe('Navigate', () => { index: 0, routes: [ { - name: SCREENS.HOME, + name: SCREENS.INBOX, }, { name: SCREENS.REPORT, diff --git a/tests/navigation/PopToSidebarTests.tsx b/tests/navigation/PopToSidebarTests.tsx index 61b7fd3e6495d..7c55dd7758b49 100644 --- a/tests/navigation/PopToSidebarTests.tsx +++ b/tests/navigation/PopToSidebarTests.tsx @@ -85,7 +85,7 @@ describe('Pop to sidebar after resize from wide to narrow layout', () => { index: 2, routes: [ { - name: SCREENS.HOME, + name: SCREENS.INBOX, }, { name: SCREENS.REPORT, diff --git a/tests/ui/BottomTabBarTest.tsx b/tests/ui/BottomTabBarTest.tsx index b4d4c5a35182a..84f7464729c25 100644 --- a/tests/ui/BottomTabBarTest.tsx +++ b/tests/ui/BottomTabBarTest.tsx @@ -93,7 +93,7 @@ describe('NavigationTabBar', () => { lastMessageText: 'Hello world!', }); - renderWithNavigation(); + renderWithNavigation(); expect(await screen.findByTestId('DebugTabView')).toBeOnTheScreen(); }); @@ -113,7 +113,7 @@ describe('NavigationTabBar', () => { lastMessageText: 'Hello world!', }); - renderWithNavigation(); + renderWithNavigation(); expect(await screen.findByTestId('DebugTabView')).toBeOnTheScreen(); }); diff --git a/tests/utils/TestNavigationContainer.tsx b/tests/utils/TestNavigationContainer.tsx index 007b8791dbd50..2545988820381 100644 --- a/tests/utils/TestNavigationContainer.tsx +++ b/tests/utils/TestNavigationContainer.tsx @@ -66,12 +66,12 @@ function TestWorkspaceSplitNavigator() { function TestReportsSplitNavigator() { return (