Skip to content

Commit e23c1c8

Browse files
authored
Merge pull request #56326 from software-mansion-labs/@szymczak/add-search-input-to-mobile-search-page
Add search input to mobile search page(with fixed performance)
2 parents 6199069 + 657375b commit e23c1c8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+822
-724
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
diff --git a/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp b/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp
2+
index 673ebd1..08d65fe 100644
3+
--- a/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp
4+
+++ b/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp
5+
@@ -710,6 +710,15 @@ void NativeReanimatedModule::performOperations() {
6+
jsPropsUpdater.call(rt, viewTag, nonAnimatableProps);
7+
}
8+
9+
+ if (propsRegistry_->shouldReanimatedSkipCommit()) {
10+
+ // It may happen that `performOperations` is called on the UI thread
11+
+ // while React Native tries to commit a new tree on the JS thread.
12+
+ // In this case, we should skip the commit here and let React Native do
13+
+ // it. The commit will include the current values from PropsRegistry which
14+
+ // will be applied in ReanimatedCommitHook.
15+
+ return;
16+
+ }
17+
+
18+
bool hasLayoutUpdates = false;
19+
for (const auto &[shadowNode, props] : copiedOperationsQueue) {
20+
if (isThereAnyLayoutProp(rt, props->asObject(rt))) {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
diff --git a/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/animationsManager.js b/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/animationsManager.js
2+
index fd6b3f5..886081c 100644
3+
--- a/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/animationsManager.js
4+
+++ b/node_modules/react-native-reanimated/lib/module/layoutReanimation/web/animationsManager.js
5+
@@ -102,9 +102,11 @@ export function tryActivateLayoutTransition(props, element, snapshot) {
6+
}
7+
const enteringAnimation = props.layout.enteringV?.presetName;
8+
const exitingAnimation = props.layout.exitingV?.presetName;
9+
+ const deltaX = (snapshot.width - rect.width) / 2;
10+
+ const deltaY = (snapshot.height - rect.height) / 2;
11+
const transitionData = {
12+
- translateX: snapshot.x - rect.x,
13+
- translateY: snapshot.y - rect.y,
14+
+ translateX: snapshot.x - rect.x + deltaX,
15+
+ translateY: snapshot.y - rect.y + deltaY,
16+
scaleX: snapshot.width / rect.width,
17+
scaleY: snapshot.height / rect.height,
18+
reversed: false,

src/ROUTES.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const ROUTES = {
3737
// This route renders the list of reports.
3838
HOME: 'home',
3939

40-
SEARCH_CENTRAL_PANE: {
40+
SEARCH_ROOT: {
4141
route: 'search',
4242
getRoute: ({query, name}: {query: SearchQueryString; name?: string}) => `search?q=${encodeURIComponent(query)}${name ? `&name=${name}` : ''}` as const,
4343
},

src/components/Navigation/BottomTabBar/index.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import useThemeStyles from '@hooks/useThemeStyles';
1919
import clearSelectedText from '@libs/clearSelectedText/clearSelectedText';
2020
import getPlatform from '@libs/getPlatform';
2121
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
22-
import * as PolicyUtils from '@libs/PolicyUtils';
23-
import * as SearchQueryUtils from '@libs/SearchQueryUtils';
22+
import {getPolicy} from '@libs/PolicyUtils';
23+
import {buildCannedSearchQuery, buildSearchQueryJSON, buildSearchQueryString} from '@libs/SearchQueryUtils';
2424
import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
2525
import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils';
2626
import {getPreservedSplitNavigatorState} from '@navigation/AppNavigator/createSplitNavigator/usePreserveSplitNavigatorState';
@@ -53,13 +53,13 @@ type BottomTabBarProps = {
5353
* Otherwise policyID will be inserted into query
5454
*/
5555
function handleQueryWithPolicyID(query: SearchQueryString, activePolicyID?: string): SearchQueryString {
56-
const queryJSON = SearchQueryUtils.buildSearchQueryJSON(query);
56+
const queryJSON = buildSearchQueryJSON(query);
5757
if (!queryJSON) {
5858
return query;
5959
}
6060

6161
const policyID = activePolicyID ?? queryJSON.policyID;
62-
const policy = PolicyUtils.getPolicy(policyID);
62+
const policy = getPolicy(policyID);
6363

6464
// In case policy is missing or there is no policy currently selected via WorkspaceSwitcher we remove it
6565
if (!activePolicyID || !policy) {
@@ -68,7 +68,7 @@ function handleQueryWithPolicyID(query: SearchQueryString, activePolicyID?: stri
6868
queryJSON.policyID = policyID;
6969
}
7070

71-
return SearchQueryUtils.buildSearchQueryString(queryJSON);
71+
return buildSearchQueryString(queryJSON);
7272
}
7373

7474
function BottomTabBar({selectedTab, isTooltipAllowed = false}: BottomTabBarProps) {
@@ -116,18 +116,18 @@ function BottomTabBar({selectedTab, isTooltipAllowed = false}: BottomTabBarProps
116116
const cleanedQuery = handleQueryWithPolicyID(q, activeWorkspaceID);
117117

118118
Navigation.navigate(
119-
ROUTES.SEARCH_CENTRAL_PANE.getRoute({
119+
ROUTES.SEARCH_ROOT.getRoute({
120120
query: cleanedQuery,
121121
...rest,
122122
}),
123123
);
124124
return;
125125
}
126126

127-
const defaultCannedQuery = SearchQueryUtils.buildCannedSearchQuery();
127+
const defaultCannedQuery = buildCannedSearchQuery();
128128
// when navigating to search we might have an activePolicyID set from workspace switcher
129129
const query = activeWorkspaceID ? `${defaultCannedQuery} ${CONST.SEARCH.SYNTAX_ROOT_KEYS.POLICY_ID}:${activeWorkspaceID}` : defaultCannedQuery;
130-
Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query}));
130+
Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query}));
131131
});
132132
}, [activeWorkspaceID, selectedTab]);
133133

src/components/Navigation/TopBar.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,25 @@ import WorkspaceSwitcherButton from '@components/WorkspaceSwitcherButton';
1010
import useLocalize from '@hooks/useLocalize';
1111
import usePolicy from '@hooks/usePolicy';
1212
import useThemeStyles from '@hooks/useThemeStyles';
13-
import Navigation from '@libs/Navigation/Navigation';
14-
import * as SearchQueryUtils from '@libs/SearchQueryUtils';
1513
import SignInButton from '@pages/home/sidebar/SignInButton';
16-
import * as Session from '@userActions/Session';
14+
import {isAnonymousUser as isAnonymousUserUtil} from '@userActions/Session';
1715
import CONST from '@src/CONST';
1816
import ONYXKEYS from '@src/ONYXKEYS';
19-
import ROUTES from '@src/ROUTES';
2017

2118
type TopBarProps = {
2219
breadcrumbLabel: string;
2320
activeWorkspaceID?: string;
2421
shouldDisplaySearch?: boolean;
25-
shouldDisplayCancelSearch?: boolean;
22+
cancelSearch?: () => void;
2623
};
2724

28-
function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, shouldDisplayCancelSearch = false}: TopBarProps) {
25+
function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, cancelSearch}: TopBarProps) {
2926
const styles = useThemeStyles();
3027
const {translate} = useLocalize();
3128
const policy = usePolicy(activeWorkspaceID);
3229
const [session] = useOnyx(ONYXKEYS.SESSION, {selector: (sessionValue) => sessionValue && {authTokenType: sessionValue.authTokenType}});
3330
const [isLoadingReportData] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA);
34-
const isAnonymousUser = Session.isAnonymousUser(session);
31+
const isAnonymousUser = isAnonymousUserUtil(session);
3532

3633
const headerBreadcrumb = policy?.name
3734
? {type: CONST.BREADCRUMB_TYPE.STRONG, text: policy.name}
@@ -63,12 +60,12 @@ function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true,
6360
</View>
6461
</View>
6562
{displaySignIn && <SignInButton />}
66-
{shouldDisplayCancelSearch && (
63+
{!!cancelSearch && (
6764
<PressableWithoutFeedback
6865
accessibilityLabel={translate('common.cancel')}
6966
style={[styles.textBlue]}
7067
onPress={() => {
71-
Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchQueryUtils.buildCannedSearchQuery()}));
68+
cancelSearch();
7269
}}
7370
>
7471
<Text style={[styles.textBlue]}>{translate('common.cancel')}</Text>

src/components/Search/SearchAutocompleteInput.tsx

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
/* eslint-disable rulesdir/no-acc-spread-in-reduce */
12
import type {ForwardedRef, ReactNode, RefObject} from 'react';
2-
import React, {forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useState} from 'react';
3+
import React, {forwardRef, useCallback, useEffect, useLayoutEffect, useMemo} from 'react';
34
import {View} from 'react-native';
45
import type {StyleProp, TextInputProps, ViewStyle} from 'react-native';
56
import {useOnyx} from 'react-native-onyx';
6-
import {useSharedValue} from 'react-native-reanimated';
7+
import Animated, {LinearTransition, useAnimatedStyle, useSharedValue} from 'react-native-reanimated';
78
import FormHelpMessage from '@components/FormHelpMessage';
89
import type {SelectionListHandle} from '@components/SelectionList/types';
910
import TextInput from '@components/TextInput';
@@ -12,6 +13,7 @@ import useActiveWorkspace from '@hooks/useActiveWorkspace';
1213
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
1314
import useLocalize from '@hooks/useLocalize';
1415
import useNetwork from '@hooks/useNetwork';
16+
import useTheme from '@hooks/useTheme';
1517
import useThemeStyles from '@hooks/useThemeStyles';
1618
import {parseFSAttributes} from '@libs/Fullstory';
1719
import runOnLiveMarkdownRuntime from '@libs/runOnLiveMarkdownRuntime';
@@ -21,8 +23,11 @@ import shouldDelayFocus from '@libs/shouldDelayFocus';
2123
import variables from '@styles/variables';
2224
import CONST from '@src/CONST';
2325
import ONYXKEYS from '@src/ONYXKEYS';
26+
import getSearchFiltersButtonTransition from './getSearchFiltersButtonTransition.ts/index';
2427
import type {SubstitutionMap} from './SearchRouter/getQueryWithSubstitutions';
2528

29+
const SearchFiltersButtonTransition = getSearchFiltersButtonTransition();
30+
2631
type SearchAutocompleteInputProps = {
2732
/** Value of TextInput */
2833
value: string;
@@ -52,10 +57,10 @@ type SearchAutocompleteInputProps = {
5257
onBlur?: () => void;
5358

5459
/** Any additional styles to apply */
55-
wrapperStyle?: StyleProp<ViewStyle>;
60+
wrapperStyle?: ViewStyle;
5661

5762
/** Any additional styles to apply when input is focused */
58-
wrapperFocusedStyle?: StyleProp<ViewStyle>;
63+
wrapperFocusedStyle?: ViewStyle;
5964

6065
/** Any additional styles to apply to text input along with FormHelperMessage */
6166
outerWrapperStyle?: StyleProp<ViewStyle>;
@@ -84,7 +89,7 @@ function SearchAutocompleteInput(
8489
onBlur,
8590
caretHidden = false,
8691
wrapperStyle,
87-
wrapperFocusedStyle,
92+
wrapperFocusedStyle = {},
8893
outerWrapperStyle,
8994
rightComponent,
9095
isSearchingForReports,
@@ -94,8 +99,8 @@ function SearchAutocompleteInput(
9499
ref: ForwardedRef<BaseTextInputRef>,
95100
) {
96101
const styles = useThemeStyles();
102+
const theme = useTheme();
97103
const {translate} = useLocalize();
98-
const [isFocused, setIsFocused] = useState<boolean>(false);
99104
const {isOffline} = useNetwork();
100105
const {activeWorkspaceID} = useActiveWorkspace();
101106
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
@@ -122,6 +127,12 @@ function SearchAutocompleteInput(
122127

123128
const offlineMessage: string = isOffline && shouldShowOfflineMessage ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : '';
124129

130+
// we are handling focused/unfocused style using shared value instead of using state to avoid re-rendering. Otherwise layout animation in `Animated.View` will lag.
131+
const focusedSharedValue = useSharedValue(false);
132+
const wrapperAnimatedStyle = useAnimatedStyle(() => {
133+
return focusedSharedValue.get() ? wrapperFocusedStyle : wrapperStyle ?? {};
134+
});
135+
125136
useEffect(() => {
126137
runOnLiveMarkdownRuntime(() => {
127138
'worklet';
@@ -170,7 +181,10 @@ function SearchAutocompleteInput(
170181

171182
return (
172183
<View style={[outerWrapperStyle]}>
173-
<View style={[styles.flexRow, styles.alignItemsCenter, wrapperStyle ?? styles.searchRouterTextInputContainer, isFocused && wrapperFocusedStyle]}>
184+
<Animated.View
185+
style={[styles.flexRow, styles.alignItemsCenter, wrapperStyle ?? styles.searchRouterTextInputContainer, wrapperAnimatedStyle]}
186+
layout={LinearTransition}
187+
>
174188
<View
175189
style={styles.flex1}
176190
fsClass={CONST.FULL_STORY.UNMASK}
@@ -195,16 +209,17 @@ function SearchAutocompleteInput(
195209
maxLength={CONST.SEARCH_QUERY_LIMIT}
196210
onSubmitEditing={onSubmit}
197211
shouldUseDisabledStyles={false}
198-
textInputContainerStyles={[styles.borderNone, styles.pb0, styles.pr3]}
199-
inputStyle={[inputWidth, styles.pl3, styles.pr3]}
212+
textInputContainerStyles={[styles.borderNone, styles.pb0]}
213+
inputStyle={[inputWidth, styles.pl3, {lineHeight: undefined}]}
214+
placeholderTextColor={theme.textSupporting}
200215
onFocus={() => {
201-
setIsFocused(true);
202-
autocompleteListRef?.current?.updateExternalTextInputFocus(true);
203216
onFocus?.();
217+
autocompleteListRef?.current?.updateExternalTextInputFocus(true);
218+
focusedSharedValue.set(true);
204219
}}
205220
onBlur={() => {
206-
setIsFocused(false);
207221
autocompleteListRef?.current?.updateExternalTextInputFocus(false);
222+
focusedSharedValue.set(false);
208223
onBlur?.();
209224
}}
210225
isLoading={!!isSearchingForReports}
@@ -216,8 +231,15 @@ function SearchAutocompleteInput(
216231
selection={selection}
217232
/>
218233
</View>
219-
{!!rightComponent && <View style={styles.pr3}>{rightComponent}</View>}
220-
</View>
234+
{!!rightComponent && (
235+
<Animated.View
236+
style={styles.pr3}
237+
layout={SearchFiltersButtonTransition}
238+
>
239+
{rightComponent}
240+
</Animated.View>
241+
)}
242+
</Animated.View>
221243
<FormHelpMessage
222244
style={styles.ph3}
223245
isError={false}

src/components/Search/SearchAutocompleteList.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,9 @@ function SearchAutocompleteList(
147147
const statusAutocompleteList = Object.values({...CONST.SEARCH.STATUS.TRIP, ...CONST.SEARCH.STATUS.INVOICE, ...CONST.SEARCH.STATUS.CHAT, ...CONST.SEARCH.STATUS.TRIP});
148148
const expenseTypes = Object.values(CONST.SEARCH.TRANSACTION_TYPE);
149149

150-
const [userCardList = {}] = useOnyx(ONYXKEYS.CARD_LIST);
151-
const [workspaceCardFeeds = {}] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST);
152-
const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds, userCardList), [userCardList, workspaceCardFeeds]);
150+
const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST);
151+
const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST);
152+
const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]);
153153
const cardAutocompleteList = Object.values(allCards);
154154

155155
const participantsAutocompleteList = useMemo(() => {

0 commit comments

Comments
 (0)