Skip to content

Commit 6a7429c

Browse files
authored
Merge pull request Expensify#82902 from callstack-internal/perf/extract-search-workspaces-tab-buttons
[No QA] Perf: Extract Search and Workspaces tab buttons to isolate Onyx subscriptions
2 parents 96e7179 + f9a685c commit 6a7429c

5 files changed

Lines changed: 400 additions & 296 deletions

File tree

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import React, {useCallback, useMemo} from 'react';
2+
import {View} from 'react-native';
3+
import type {ValueOf} from 'type-fest';
4+
import Icon from '@components/Icon';
5+
import {PressableWithFeedback} from '@components/Pressable';
6+
import Text from '@components/Text';
7+
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
8+
import useLocalize from '@hooks/useLocalize';
9+
import useOnyx from '@hooks/useOnyx';
10+
import useSearchTypeMenuSections from '@hooks/useSearchTypeMenuSections';
11+
import useTheme from '@hooks/useTheme';
12+
import useThemeStyles from '@hooks/useThemeStyles';
13+
import clearSelectedText from '@libs/clearSelectedText/clearSelectedText';
14+
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
15+
import Navigation from '@libs/Navigation/Navigation';
16+
import {buildCannedSearchQuery, buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUtils';
17+
import {getDefaultActionableSearchMenuItem} from '@libs/SearchUIUtils';
18+
import {startSpan} from '@libs/telemetry/activeSpans';
19+
import navigationRef from '@navigation/navigationRef';
20+
import type {SearchFullscreenNavigatorParamList} from '@navigation/types';
21+
import variables from '@styles/variables';
22+
import CONST from '@src/CONST';
23+
import NAVIGATORS from '@src/NAVIGATORS';
24+
import ONYXKEYS from '@src/ONYXKEYS';
25+
import ROUTES from '@src/ROUTES';
26+
import SCREENS from '@src/SCREENS';
27+
import getLastRoute from './getLastRoute';
28+
import getTabIconFill from './getTabIconFill';
29+
import NAVIGATION_TABS from './NAVIGATION_TABS';
30+
31+
type SearchTabButtonProps = {
32+
selectedTab: ValueOf<typeof NAVIGATION_TABS>;
33+
isWideLayout: boolean;
34+
};
35+
36+
function SearchTabButton({selectedTab, isWideLayout}: SearchTabButtonProps) {
37+
const theme = useTheme();
38+
const styles = useThemeStyles();
39+
const {translate} = useLocalize();
40+
const expensifyIcons = useMemoizedLazyExpensifyIcons(['MoneySearch']);
41+
const {typeMenuSections} = useSearchTypeMenuSections();
42+
const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES, {canBeMissing: true});
43+
const [lastSearchParams] = useOnyx(ONYXKEYS.REPORT_NAVIGATION_LAST_SEARCH_QUERY, {canBeMissing: true});
44+
45+
const searchAccessibilityState = useMemo(() => ({selected: selectedTab === NAVIGATION_TABS.SEARCH}), [selectedTab]);
46+
const lastQueryJSON = lastSearchParams?.queryJSON;
47+
48+
const navigateToSearch = useCallback(() => {
49+
if (selectedTab === NAVIGATION_TABS.SEARCH) {
50+
return;
51+
}
52+
clearSelectedText();
53+
interceptAnonymousUser(() => {
54+
const parentSpan = startSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB, {
55+
name: CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB,
56+
op: CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB,
57+
});
58+
parentSpan?.setAttribute(CONST.TELEMETRY.ATTRIBUTE_ROUTE_FROM, selectedTab ?? '');
59+
60+
startSpan(CONST.TELEMETRY.SPAN_ON_LAYOUT_SKELETON_REPORTS, {
61+
name: CONST.TELEMETRY.SPAN_ON_LAYOUT_SKELETON_REPORTS,
62+
op: CONST.TELEMETRY.SPAN_ON_LAYOUT_SKELETON_REPORTS,
63+
parentSpan,
64+
});
65+
66+
const lastSearchRoute = getLastRoute(navigationRef.getRootState(), NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR, SCREENS.SEARCH.ROOT);
67+
68+
if (lastSearchRoute) {
69+
const {q, ...rest} = lastSearchRoute.params as SearchFullscreenNavigatorParamList[typeof SCREENS.SEARCH.ROOT];
70+
const queryJSON = buildSearchQueryJSON(q);
71+
if (queryJSON) {
72+
const query = buildSearchQueryString(queryJSON);
73+
Navigation.navigate(
74+
ROUTES.SEARCH_ROOT.getRoute({
75+
query,
76+
...rest,
77+
}),
78+
);
79+
return;
80+
}
81+
}
82+
83+
const flattenedMenuItems = typeMenuSections.flatMap((section) => section.menuItems);
84+
const defaultActionableSearchQuery =
85+
getDefaultActionableSearchMenuItem(flattenedMenuItems)?.searchQuery ?? flattenedMenuItems.at(0)?.searchQuery ?? typeMenuSections.at(0)?.menuItems.at(0)?.searchQuery;
86+
87+
const savedSearchQuery = Object.values(savedSearches ?? {}).at(0)?.query;
88+
const lastQueryFromOnyx = lastQueryJSON ? buildSearchQueryString(lastQueryJSON) : undefined;
89+
Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: lastQueryFromOnyx ?? defaultActionableSearchQuery ?? savedSearchQuery ?? buildCannedSearchQuery()}));
90+
});
91+
}, [selectedTab, typeMenuSections, savedSearches, lastQueryJSON]);
92+
93+
if (isWideLayout) {
94+
return (
95+
<PressableWithFeedback
96+
onPress={navigateToSearch}
97+
role={CONST.ROLE.TAB}
98+
accessibilityLabel={translate('common.reports')}
99+
accessibilityState={searchAccessibilityState}
100+
style={({hovered}) => [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]}
101+
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.REPORTS}
102+
>
103+
{({hovered}) => (
104+
<>
105+
<View>
106+
<Icon
107+
src={expensifyIcons.MoneySearch}
108+
fill={getTabIconFill(theme, {isSelected: selectedTab === NAVIGATION_TABS.SEARCH, isHovered: hovered})}
109+
width={variables.iconBottomBar}
110+
height={variables.iconBottomBar}
111+
/>
112+
</View>
113+
<Text
114+
numberOfLines={2}
115+
style={[
116+
styles.textSmall,
117+
styles.textAlignCenter,
118+
styles.mt1Half,
119+
selectedTab === NAVIGATION_TABS.SEARCH ? styles.textBold : styles.textSupporting,
120+
styles.navigationTabBarLabel,
121+
]}
122+
>
123+
{translate('common.reports')}
124+
</Text>
125+
</>
126+
)}
127+
</PressableWithFeedback>
128+
);
129+
}
130+
131+
return (
132+
<PressableWithFeedback
133+
onPress={navigateToSearch}
134+
role={CONST.ROLE.TAB}
135+
accessibilityLabel={translate('common.reports')}
136+
accessibilityState={searchAccessibilityState}
137+
wrapperStyle={styles.flex1}
138+
style={styles.navigationTabBarItem}
139+
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.REPORTS}
140+
>
141+
<View>
142+
<Icon
143+
src={expensifyIcons.MoneySearch}
144+
fill={selectedTab === NAVIGATION_TABS.SEARCH ? theme.iconMenu : theme.icon}
145+
width={variables.iconBottomBar}
146+
height={variables.iconBottomBar}
147+
/>
148+
</View>
149+
<Text
150+
numberOfLines={1}
151+
style={[
152+
styles.textSmall,
153+
styles.textAlignCenter,
154+
styles.mt1Half,
155+
selectedTab === NAVIGATION_TABS.SEARCH ? styles.textBold : styles.textSupporting,
156+
styles.navigationTabBarLabel,
157+
]}
158+
>
159+
{translate('common.reports')}
160+
</Text>
161+
</PressableWithFeedback>
162+
);
163+
}
164+
165+
export default SearchTabButton;
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import {findFocusedRoute, useNavigationState} from '@react-navigation/native';
2+
import React, {useCallback, useMemo} from 'react';
3+
import {View} from 'react-native';
4+
import type {OnyxCollection} from 'react-native-onyx';
5+
import type {ValueOf} from 'type-fest';
6+
import Icon from '@components/Icon';
7+
import {PressableWithFeedback} from '@components/Pressable';
8+
import Text from '@components/Text';
9+
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
10+
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
11+
import useLocalize from '@hooks/useLocalize';
12+
import useOnyx from '@hooks/useOnyx';
13+
import useResponsiveLayout from '@hooks/useResponsiveLayout';
14+
import useTheme from '@hooks/useTheme';
15+
import useThemeStyles from '@hooks/useThemeStyles';
16+
import useWorkspacesTabIndicatorStatus from '@hooks/useWorkspacesTabIndicatorStatus';
17+
import navigateToWorkspacesPage, {getWorkspaceNavigationRouteState} from '@libs/Navigation/helpers/navigateToWorkspacesPage';
18+
import variables from '@styles/variables';
19+
import CONST from '@src/CONST';
20+
import NAVIGATORS from '@src/NAVIGATORS';
21+
import ONYXKEYS from '@src/ONYXKEYS';
22+
import type {DomainSplitNavigatorParamList, WorkspaceSplitNavigatorParamList} from '@navigation/types';
23+
import type SCREENS from '@src/SCREENS';
24+
import type {Domain, Policy} from '@src/types/onyx';
25+
import getTabIconFill from './getTabIconFill';
26+
import NAVIGATION_TABS from './NAVIGATION_TABS';
27+
28+
type WorkspacesTabButtonProps = {
29+
selectedTab: ValueOf<typeof NAVIGATION_TABS>;
30+
isWideLayout: boolean;
31+
};
32+
33+
function WorkspacesTabButton({selectedTab, isWideLayout}: WorkspacesTabButtonProps) {
34+
const theme = useTheme();
35+
const styles = useThemeStyles();
36+
const {translate, preferredLocale} = useLocalize();
37+
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Buildings']);
38+
const {indicatorColor: workspacesTabIndicatorColor, status: workspacesTabIndicatorStatus} = useWorkspacesTabIndicatorStatus();
39+
const {shouldUseNarrowLayout} = useResponsiveLayout();
40+
const {login: currentUserLogin} = useCurrentUserPersonalDetails();
41+
42+
const navigationState = useNavigationState(findFocusedRoute);
43+
const {lastWorkspacesTabNavigatorRoute, workspacesTabState} = getWorkspaceNavigationRouteState();
44+
const params = workspacesTabState?.routes?.at(0)?.params as
45+
| WorkspaceSplitNavigatorParamList[typeof SCREENS.WORKSPACE.INITIAL]
46+
| DomainSplitNavigatorParamList[typeof SCREENS.DOMAIN.INITIAL];
47+
48+
const paramsPolicyID = params && 'policyID' in params ? params.policyID : undefined;
49+
const paramsDomainAccountID = params && 'domainAccountID' in params ? params.domainAccountID : undefined;
50+
51+
const lastViewedPolicySelector = useCallback(
52+
(policies: OnyxCollection<Policy>) => {
53+
if (!lastWorkspacesTabNavigatorRoute || lastWorkspacesTabNavigatorRoute.name !== NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR || !paramsPolicyID) {
54+
return undefined;
55+
}
56+
57+
return policies?.[`${ONYXKEYS.COLLECTION.POLICY}${paramsPolicyID}`];
58+
},
59+
[paramsPolicyID, lastWorkspacesTabNavigatorRoute],
60+
);
61+
62+
const [lastViewedPolicy] = useOnyx(
63+
ONYXKEYS.COLLECTION.POLICY,
64+
{
65+
canBeMissing: true,
66+
selector: lastViewedPolicySelector,
67+
},
68+
[navigationState],
69+
);
70+
71+
const lastViewedDomainSelector = useCallback(
72+
(domains: OnyxCollection<Domain>) => {
73+
if (!lastWorkspacesTabNavigatorRoute || lastWorkspacesTabNavigatorRoute.name !== NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR || !paramsDomainAccountID) {
74+
return undefined;
75+
}
76+
77+
return domains?.[`${ONYXKEYS.COLLECTION.DOMAIN}${paramsDomainAccountID}`];
78+
},
79+
[paramsDomainAccountID, lastWorkspacesTabNavigatorRoute],
80+
);
81+
82+
const [lastViewedDomain] = useOnyx(
83+
ONYXKEYS.COLLECTION.DOMAIN,
84+
{
85+
canBeMissing: true,
86+
selector: lastViewedDomainSelector,
87+
},
88+
[navigationState],
89+
);
90+
91+
const showWorkspaces = useCallback(() => {
92+
navigateToWorkspacesPage({shouldUseNarrowLayout, currentUserLogin, policy: lastViewedPolicy, domain: lastViewedDomain});
93+
}, [shouldUseNarrowLayout, currentUserLogin, lastViewedPolicy, lastViewedDomain]);
94+
95+
const workspacesAccessibilityState = useMemo(() => ({selected: selectedTab === NAVIGATION_TABS.WORKSPACES}), [selectedTab]);
96+
97+
if (isWideLayout) {
98+
return (
99+
<PressableWithFeedback
100+
onPress={showWorkspaces}
101+
role={CONST.ROLE.TAB}
102+
accessibilityLabel={`${translate('common.workspacesTabTitle')}${workspacesTabIndicatorStatus ? `. ${translate('common.yourReviewIsRequired')}` : ''}`}
103+
accessibilityState={workspacesAccessibilityState}
104+
style={({hovered}) => [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]}
105+
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.WORKSPACES}
106+
>
107+
{({hovered}) => (
108+
<>
109+
<View>
110+
<Icon
111+
src={expensifyIcons.Buildings}
112+
fill={getTabIconFill(theme, {isSelected: selectedTab === NAVIGATION_TABS.WORKSPACES, isHovered: hovered})}
113+
width={variables.iconBottomBar}
114+
height={variables.iconBottomBar}
115+
/>
116+
{!!workspacesTabIndicatorStatus && (
117+
<View
118+
style={[styles.navigationTabBarStatusIndicator, styles.statusIndicatorColor(workspacesTabIndicatorColor), hovered && {borderColor: theme.sidebarHover}]}
119+
/>
120+
)}
121+
</View>
122+
<Text
123+
numberOfLines={preferredLocale === CONST.LOCALES.DE || preferredLocale === CONST.LOCALES.NL ? 1 : 2}
124+
style={[
125+
styles.textSmall,
126+
styles.textAlignCenter,
127+
styles.mt1Half,
128+
selectedTab === NAVIGATION_TABS.WORKSPACES ? styles.textBold : styles.textSupporting,
129+
styles.navigationTabBarLabel,
130+
]}
131+
>
132+
{translate('common.workspacesTabTitle')}
133+
</Text>
134+
</>
135+
)}
136+
</PressableWithFeedback>
137+
);
138+
}
139+
140+
return (
141+
<PressableWithFeedback
142+
onPress={showWorkspaces}
143+
role={CONST.ROLE.TAB}
144+
accessibilityLabel={`${translate('common.workspacesTabTitle')}${workspacesTabIndicatorStatus ? `. ${translate('common.yourReviewIsRequired')}` : ''}`}
145+
accessibilityState={workspacesAccessibilityState}
146+
wrapperStyle={styles.flex1}
147+
style={styles.navigationTabBarItem}
148+
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.WORKSPACES}
149+
>
150+
<View>
151+
<Icon
152+
src={expensifyIcons.Buildings}
153+
fill={selectedTab === NAVIGATION_TABS.WORKSPACES ? theme.iconMenu : theme.icon}
154+
width={variables.iconBottomBar}
155+
height={variables.iconBottomBar}
156+
/>
157+
{!!workspacesTabIndicatorStatus && <View style={[styles.navigationTabBarStatusIndicator, styles.statusIndicatorColor(workspacesTabIndicatorColor)]} />}
158+
</View>
159+
<Text
160+
numberOfLines={1}
161+
style={[
162+
styles.textSmall,
163+
styles.textAlignCenter,
164+
styles.mt1Half,
165+
selectedTab === NAVIGATION_TABS.WORKSPACES ? styles.textBold : styles.textSupporting,
166+
styles.navigationTabBarLabel,
167+
]}
168+
>
169+
{translate('common.workspacesTabTitle')}
170+
</Text>
171+
</PressableWithFeedback>
172+
);
173+
}
174+
175+
export default WorkspacesTabButton;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type {NavigationState} from '@react-navigation/native';
2+
import type {ValueOf} from 'type-fest';
3+
import {getPreservedNavigatorState} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState';
4+
import type NAVIGATORS from '@src/NAVIGATORS';
5+
import type {Screen} from '@src/SCREENS';
6+
7+
function getLastRoute(rootState: NavigationState, navigator: ValueOf<typeof NAVIGATORS>, screen: Screen) {
8+
const lastNavigator = rootState.routes.findLast((route) => route.name === navigator);
9+
const lastNavigatorState = lastNavigator?.key ? getPreservedNavigatorState(lastNavigator.key) : undefined;
10+
const lastRoute = lastNavigatorState?.routes.findLast((route) => route.name === screen);
11+
return lastRoute;
12+
}
13+
14+
export default getLastRoute;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type {ThemeColors} from '@styles/theme/types';
2+
3+
type GetTabIconFillConfig = {
4+
isSelected: boolean;
5+
isHovered: boolean;
6+
};
7+
8+
function getTabIconFill(theme: ThemeColors, {isSelected, isHovered}: GetTabIconFillConfig): string {
9+
if (isSelected) {
10+
return theme.iconMenu;
11+
}
12+
if (isHovered) {
13+
return theme.success;
14+
}
15+
return theme.icon;
16+
}
17+
18+
export default getTabIconFill;

0 commit comments

Comments
 (0)