Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions src/components/Navigation/NavigationTabBar/SearchTabButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import React, {useCallback, useMemo} from 'react';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
import Icon from '@components/Icon';
import {PressableWithFeedback} from '@components/Pressable';
import Text from '@components/Text';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useSearchTypeMenuSections from '@hooks/useSearchTypeMenuSections';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import clearSelectedText from '@libs/clearSelectedText/clearSelectedText';
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
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 navigationRef from '@navigation/navigationRef';
import type {SearchFullscreenNavigatorParamList} from '@navigation/types';
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 getLastRoute from './getLastRoute';
import getTabIconFill from './getTabIconFill';
import NAVIGATION_TABS from './NAVIGATION_TABS';

type SearchTabButtonProps = {
selectedTab: ValueOf<typeof NAVIGATION_TABS>;
isWideLayout: boolean;
};

function SearchTabButton({selectedTab, isWideLayout}: SearchTabButtonProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
const expensifyIcons = useMemoizedLazyExpensifyIcons(['MoneySearch']);
const {typeMenuSections} = useSearchTypeMenuSections();
const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true});
const [lastSearchParams] = useOnyx(ONYXKEYS.REPORT_NAVIGATION_LAST_SEARCH_QUERY, {canBeMissing: true});

const searchAccessibilityState = useMemo(() => ({selected: selectedTab === NAVIGATION_TABS.SEARCH}), [selectedTab]);
const lastQueryJSON = lastSearchParams?.queryJSON;

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,
});
parentSpan?.setAttribute(CONST.TELEMETRY.ATTRIBUTE_ROUTE_FROM, selectedTab ?? '');

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 lastSearchRoute = getLastRoute(navigationRef.getRootState(), NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR, 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 = lastQueryJSON ? buildSearchQueryString(lastQueryJSON) : undefined;
Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: lastQueryFromOnyx ?? defaultActionableSearchQuery ?? savedSearchQuery ?? buildCannedSearchQuery()}));
});
}, [selectedTab, typeMenuSections, savedSearches, lastQueryJSON]);

if (isWideLayout) {
return (
<PressableWithFeedback
onPress={navigateToSearch}
role={CONST.ROLE.TAB}
accessibilityLabel={translate('common.reports')}
accessibilityState={searchAccessibilityState}
style={({hovered}) => [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]}
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.REPORTS}
>
{({hovered}) => (
<>
<View>
<Icon
src={expensifyIcons.MoneySearch}
fill={getTabIconFill(theme, {isSelected: selectedTab === NAVIGATION_TABS.SEARCH, isHovered: hovered})}
width={variables.iconBottomBar}
height={variables.iconBottomBar}
/>
</View>
<Text
numberOfLines={2}
style={[
styles.textSmall,
styles.textAlignCenter,
styles.mt1Half,
selectedTab === NAVIGATION_TABS.SEARCH ? styles.textBold : styles.textSupporting,
styles.navigationTabBarLabel,
]}
>
{translate('common.reports')}
</Text>
</>
)}
</PressableWithFeedback>
);
}

return (
<PressableWithFeedback
onPress={navigateToSearch}
role={CONST.ROLE.TAB}
accessibilityLabel={translate('common.reports')}
accessibilityState={searchAccessibilityState}
wrapperStyle={styles.flex1}
style={styles.navigationTabBarItem}
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.REPORTS}
>
<View>
<Icon
src={expensifyIcons.MoneySearch}
fill={selectedTab === NAVIGATION_TABS.SEARCH ? theme.iconMenu : theme.icon}
width={variables.iconBottomBar}
height={variables.iconBottomBar}
/>
</View>
<Text
numberOfLines={1}
style={[
styles.textSmall,
styles.textAlignCenter,
styles.mt1Half,
selectedTab === NAVIGATION_TABS.SEARCH ? styles.textBold : styles.textSupporting,
styles.navigationTabBarLabel,
]}
>
{translate('common.reports')}
</Text>
</PressableWithFeedback>
);
}

export default SearchTabButton;
175 changes: 175 additions & 0 deletions src/components/Navigation/NavigationTabBar/WorkspacesTabButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import {findFocusedRoute, useNavigationState} from '@react-navigation/native';
import React, {useCallback, useMemo} from 'react';
import {View} from 'react-native';
import type {OnyxCollection} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import Icon from '@components/Icon';
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 useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWorkspacesTabIndicatorStatus from '@hooks/useWorkspacesTabIndicatorStatus';
import navigateToWorkspacesPage, {getWorkspaceNavigationRouteState} from '@libs/Navigation/helpers/navigateToWorkspacesPage';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
import type {DomainSplitNavigatorParamList, WorkspaceSplitNavigatorParamList} from '@navigation/types';
import type SCREENS from '@src/SCREENS';
import type {Domain, Policy} from '@src/types/onyx';
import getTabIconFill from './getTabIconFill';
import NAVIGATION_TABS from './NAVIGATION_TABS';

type WorkspacesTabButtonProps = {
selectedTab: ValueOf<typeof NAVIGATION_TABS>;
isWideLayout: boolean;
};

function WorkspacesTabButton({selectedTab, isWideLayout}: WorkspacesTabButtonProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate, preferredLocale} = useLocalize();
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Buildings']);
const {indicatorColor: workspacesTabIndicatorColor, status: workspacesTabIndicatorStatus} = useWorkspacesTabIndicatorStatus();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {login: currentUserLogin} = useCurrentUserPersonalDetails();

const navigationState = useNavigationState(findFocusedRoute);
const {lastWorkspacesTabNavigatorRoute, workspacesTabState} = getWorkspaceNavigationRouteState();
const params = workspacesTabState?.routes?.at(0)?.params as
| WorkspaceSplitNavigatorParamList[typeof SCREENS.WORKSPACE.INITIAL]
| DomainSplitNavigatorParamList[typeof SCREENS.DOMAIN.INITIAL];

const paramsPolicyID = params && 'policyID' in params ? params.policyID : undefined;
const paramsDomainAccountID = params && 'domainAccountID' in params ? params.domainAccountID : undefined;

const lastViewedPolicySelector = useCallback(
(policies: OnyxCollection<Policy>) => {
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<Domain>) => {
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 showWorkspaces = useCallback(() => {
navigateToWorkspacesPage({shouldUseNarrowLayout, currentUserLogin, policy: lastViewedPolicy, domain: lastViewedDomain});
}, [shouldUseNarrowLayout, currentUserLogin, lastViewedPolicy, lastViewedDomain]);

const workspacesAccessibilityState = useMemo(() => ({selected: selectedTab === NAVIGATION_TABS.WORKSPACES}), [selectedTab]);

if (isWideLayout) {
return (
<PressableWithFeedback
onPress={showWorkspaces}
role={CONST.ROLE.TAB}
accessibilityLabel={`${translate('common.workspacesTabTitle')}${workspacesTabIndicatorStatus ? `. ${translate('common.yourReviewIsRequired')}` : ''}`}
accessibilityState={workspacesAccessibilityState}
style={({hovered}) => [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]}
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.WORKSPACES}
>
{({hovered}) => (
<>
<View>
<Icon
src={expensifyIcons.Buildings}
fill={getTabIconFill(theme, {isSelected: selectedTab === NAVIGATION_TABS.WORKSPACES, isHovered: hovered})}
width={variables.iconBottomBar}
height={variables.iconBottomBar}
/>
{!!workspacesTabIndicatorStatus && (
<View
style={[styles.navigationTabBarStatusIndicator, styles.statusIndicatorColor(workspacesTabIndicatorColor), hovered && {borderColor: theme.sidebarHover}]}
/>
)}
</View>
<Text
numberOfLines={preferredLocale === CONST.LOCALES.DE || preferredLocale === CONST.LOCALES.NL ? 1 : 2}
style={[
styles.textSmall,
styles.textAlignCenter,
styles.mt1Half,
selectedTab === NAVIGATION_TABS.WORKSPACES ? styles.textBold : styles.textSupporting,
styles.navigationTabBarLabel,
]}
>
{translate('common.workspacesTabTitle')}
</Text>
</>
)}
</PressableWithFeedback>
);
}

return (
<PressableWithFeedback
onPress={showWorkspaces}
role={CONST.ROLE.TAB}
accessibilityLabel={`${translate('common.workspacesTabTitle')}${workspacesTabIndicatorStatus ? `. ${translate('common.yourReviewIsRequired')}` : ''}`}
accessibilityState={workspacesAccessibilityState}
wrapperStyle={styles.flex1}
style={styles.navigationTabBarItem}
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.WORKSPACES}
>
<View>
<Icon
src={expensifyIcons.Buildings}
fill={selectedTab === NAVIGATION_TABS.WORKSPACES ? theme.iconMenu : theme.icon}
width={variables.iconBottomBar}
height={variables.iconBottomBar}
/>
{!!workspacesTabIndicatorStatus && <View style={[styles.navigationTabBarStatusIndicator, styles.statusIndicatorColor(workspacesTabIndicatorColor)]} />}
</View>
<Text
numberOfLines={1}
style={[
styles.textSmall,
styles.textAlignCenter,
styles.mt1Half,
selectedTab === NAVIGATION_TABS.WORKSPACES ? styles.textBold : styles.textSupporting,
styles.navigationTabBarLabel,
]}
>
{translate('common.workspacesTabTitle')}
</Text>
</PressableWithFeedback>
);
}

export default WorkspacesTabButton;
14 changes: 14 additions & 0 deletions src/components/Navigation/NavigationTabBar/getLastRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type {NavigationState} from '@react-navigation/native';
import type {ValueOf} from 'type-fest';
import {getPreservedNavigatorState} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState';
import type NAVIGATORS from '@src/NAVIGATORS';
import type {Screen} from '@src/SCREENS';

function getLastRoute(rootState: NavigationState, navigator: ValueOf<typeof NAVIGATORS>, screen: Screen) {
const lastNavigator = rootState.routes.findLast((route) => route.name === navigator);
const lastNavigatorState = lastNavigator?.key ? getPreservedNavigatorState(lastNavigator.key) : undefined;
const lastRoute = lastNavigatorState?.routes.findLast((route) => route.name === screen);
return lastRoute;
}

export default getLastRoute;
18 changes: 18 additions & 0 deletions src/components/Navigation/NavigationTabBar/getTabIconFill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type {ThemeColors} from '@styles/theme/types';

type GetTabIconFillConfig = {
isSelected: boolean;
isHovered: boolean;
};

function getTabIconFill(theme: ThemeColors, {isSelected, isHovered}: GetTabIconFillConfig): string {
if (isSelected) {
return theme.iconMenu;
}
if (isHovered) {
return theme.success;
}
return theme.icon;
}

export default getTabIconFill;
Loading
Loading