Skip to content

Commit 9f96f08

Browse files
authored
Merge pull request Expensify#86982 from software-mansion-labs/fix/newChatPage/dataPreparation/extractSearchMatchUtil
Refactor searchMatchUtils out from optionsListUtils
2 parents 22b7c39 + e975331 commit 9f96f08

8 files changed

Lines changed: 236 additions & 54 deletions

File tree

src/hooks/useSearchSelector.base.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import type {PermissionStatus} from 'react-native-permissions';
55
import {usePersonalDetails} from '@components/OnyxListItemProvider';
66
import {useOptionsList} from '@components/OptionListContextProvider';
77
import type {GetOptionsConfig, Option, Options, SearchOption} from '@libs/OptionsListUtils';
8-
import {getEmptyOptions, getPersonalDetailSearchTerms, getSearchOptions, getSearchValueForPhoneOrEmail, getValidOptions} from '@libs/OptionsListUtils';
8+
import {getEmptyOptions, getSearchOptions, getSearchValueForPhoneOrEmail, getValidOptions} from '@libs/OptionsListUtils';
9+
import {getPersonalDetailSearchTerms} from '@libs/OptionsListUtils/searchMatchUtils';
910
import type {OptionData} from '@libs/ReportUtils';
1011
import CONST from '@src/CONST';
1112
import ONYXKEYS from '@src/ONYXKEYS';

src/libs/OptionsListUtils/index.ts

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ import type {
188188
} from '@src/types/onyx';
189189
import type {Attendee, Participant} from '@src/types/onyx/IOU';
190190
import {isEmptyObject} from '@src/types/utils/EmptyObject';
191+
import {doesPersonalDetailMatchSearchTerm, getCurrentUserSearchTerms, getPersonalDetailSearchTerms} from './searchMatchUtils';
191192
import type {
192193
FilterUserToInviteConfig,
193194
GetOptionsConfig,
@@ -2668,10 +2669,12 @@ function getValidOptions(
26682669
if (personalDetailLoginsToExclude[personalDetail.login]) {
26692670
return false;
26702671
}
2671-
const personalDetailSearchTerms = getPersonalDetailSearchTerms(personalDetail, currentUserAccountID);
2672-
const searchText = deburr(`${personalDetailSearchTerms.join(' ')} ${personalDetail.text ?? ''}`.toLocaleLowerCase());
2673-
2674-
return searchTerms.every((term) => searchText.includes(term));
2672+
return searchTerms.every((term) =>
2673+
doesPersonalDetailMatchSearchTerm(personalDetail, currentUserAccountID, term, {
2674+
useLocaleLowerCase: true,
2675+
transformSearchText: (concatenatedSearchTerms) => deburr(`${concatenatedSearchTerms} ${(personalDetail.text ?? '').toLocaleLowerCase()}`),
2676+
}),
2677+
);
26752678
};
26762679

26772680
// when we expect that function return eg. 50 elements and we already found 40 recent reports, we should adjust the max personal details number.
@@ -3009,7 +3012,7 @@ function formatSectionsFromSearchTerm(
30093012
// This will add them to the list of options, deduping them if they already exist in the other lists
30103013
const selectedParticipantsWithoutDetails = selectedOptions.filter((participant) => {
30113014
const accountID = participant.accountID ?? null;
3012-
const isPartOfSearchTerm = getPersonalDetailSearchTerms(participant, currentUserAccountID).join(' ').toLowerCase().includes(cleanSearchTerm);
3015+
const isPartOfSearchTerm = doesPersonalDetailMatchSearchTerm(participant, currentUserAccountID, cleanSearchTerm);
30133016
const isReportInRecentReports = filteredRecentReports.some((report) => report.accountID === accountID) || filteredWorkspaceChats.some((report) => report.accountID === accountID);
30143017
const isReportInPersonalDetails = filteredPersonalDetails.some((personalDetail) => personalDetail.accountID === accountID);
30153018

@@ -3037,18 +3040,6 @@ function formatSectionsFromSearchTerm(
30373040
};
30383041
}
30393042

3040-
function getPersonalDetailSearchTerms(item: Partial<SearchOptionData>, currentUserAccountID: number) {
3041-
if (item.accountID === currentUserAccountID) {
3042-
return getCurrentUserSearchTerms(item);
3043-
}
3044-
return [item.participantsList?.[0]?.displayName ?? item.displayName ?? '', item.login ?? '', item.login?.replace(CONST.EMAIL_SEARCH_REGEX, '') ?? ''];
3045-
}
3046-
3047-
function getCurrentUserSearchTerms(item: Partial<SearchOptionData>) {
3048-
// eslint-disable-next-line @typescript-eslint/no-deprecated
3049-
return [item.text ?? item.displayName ?? '', item.login ?? '', item.login?.replace(CONST.EMAIL_SEARCH_REGEX, '') ?? '', translateLocal('common.you'), translateLocal('common.me')];
3050-
}
3051-
30523043
/**
30533044
* Remove the personal details for the DMs that are already in the recent reports so that we don't show duplicates.
30543045
*/
@@ -3441,7 +3432,6 @@ export {
34413432
formatSectionsFromSearchTerm,
34423433
getAlternateText,
34433434
getFilteredRecentAttendees,
3444-
getCurrentUserSearchTerms,
34453435
getEmptyOptions,
34463436
getHeaderMessage,
34473437
getHeaderMessageForNonUserList,
@@ -3453,7 +3443,6 @@ export {
34533443
getLastMessageTextForReport,
34543444
getManagerMcTestParticipant,
34553445
getParticipantsOption,
3456-
getPersonalDetailSearchTerms,
34573446
getPersonalDetailsForAccountIDs,
34583447
getPolicyExpenseReportOption,
34593448
getReportDisplayOption,
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// eslint-disable-next-line @typescript-eslint/no-deprecated
2+
import {translateLocal} from '@libs/Localize';
3+
import CONST from '@src/CONST';
4+
import type {SearchOptionData} from './types';
5+
6+
type SearchMatchConfig = {
7+
/** Whether to use toLocaleLowerCase() instead of toLowerCase(), defaults to false */
8+
useLocaleLowerCase?: boolean;
9+
10+
/**
11+
* Optional callback to transform the concatenated search terms before matching.
12+
* @param concatenatedSearchTerms - the joined terms string, already lowercased
13+
*/
14+
transformSearchText?: (concatenatedSearchTerms: string) => string;
15+
};
16+
17+
/**
18+
* Includes localized "You"/"Me" so the current user is findable
19+
* by those terms in any supported language.
20+
*
21+
* @returns Raw (not lowercased) terms: display text, login,
22+
* login with dots stripped before @, and translated "You"/"Me".
23+
*/
24+
function getCurrentUserSearchTerms(item: Partial<SearchOptionData>) {
25+
// eslint-disable-next-line @typescript-eslint/no-deprecated
26+
return [item.text ?? item.displayName ?? '', item.login ?? '', item.login?.replace(CONST.EMAIL_SEARCH_REGEX, '') ?? '', translateLocal('common.you'), translateLocal('common.me')];
27+
}
28+
29+
/**
30+
* For the current user, delegates to getCurrentUserSearchTerms.
31+
* For others, includes display name and login with dots stripped
32+
* before @ (so "john.doe@" matches "johndoe@").
33+
*
34+
* @returns Raw (not lowercased) terms the person is searchable by.
35+
*/
36+
function getPersonalDetailSearchTerms(item: Partial<SearchOptionData>, currentUserAccountID: number) {
37+
if (item.accountID === currentUserAccountID) {
38+
return getCurrentUserSearchTerms(item);
39+
}
40+
return [item.participantsList?.[0]?.displayName ?? item.displayName ?? '', item.login ?? '', item.login?.replace(CONST.EMAIL_SEARCH_REGEX, '') ?? ''];
41+
}
42+
43+
/**
44+
* Checks whether a personal detail option matches a single search term
45+
* by comparing against the option's searchable fields (displayName, login, etc.).
46+
*
47+
* Expects `searchTerm` to already be lowercased and trimmed.
48+
*/
49+
function doesPersonalDetailMatchSearchTerm(
50+
item: Partial<SearchOptionData>,
51+
currentUserAccountID: number,
52+
searchTerm: string,
53+
{useLocaleLowerCase = false, transformSearchText}: SearchMatchConfig = {},
54+
): boolean {
55+
const terms = getPersonalDetailSearchTerms(item, currentUserAccountID).join(' ');
56+
let searchText = useLocaleLowerCase ? terms.toLocaleLowerCase() : terms.toLowerCase();
57+
58+
if (transformSearchText) {
59+
searchText = transformSearchText(searchText);
60+
}
61+
62+
return searchText.includes(searchTerm);
63+
}
64+
65+
export {getCurrentUserSearchTerms, getPersonalDetailSearchTerms, doesPersonalDetailMatchSearchTerm};
66+
export type {SearchMatchConfig};

src/pages/NewChatPage.tsx

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,15 @@ import useIsFocusedRef from '@hooks/useIsFocusedRef';
2424
import useLocalize from '@hooks/useLocalize';
2525
import useNetwork from '@hooks/useNetwork';
2626
import useOnyx from '@hooks/useOnyx';
27-
import usePrivateIsArchivedMap from '@hooks/usePrivateIsArchivedMap';
2827
import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
2928
import useSingleExecution from '@hooks/useSingleExecution';
3029
import useThemeStyles from '@hooks/useThemeStyles';
3130
import {navigateToAndOpenReport, searchInServer, setGroupDraft} from '@libs/actions/Report';
3231
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
3332
import Log from '@libs/Log';
3433
import Navigation from '@libs/Navigation/Navigation';
35-
import {
36-
filterAndOrderOptions,
37-
filterSelectedOptions,
38-
formatSectionsFromSearchTerm,
39-
getHeaderMessage,
40-
getPersonalDetailSearchTerms,
41-
getUserToInviteOption,
42-
getValidOptions,
43-
} from '@libs/OptionsListUtils';
34+
import {filterAndOrderOptions, filterSelectedOptions, getHeaderMessage, getUserToInviteOption, getValidOptions} from '@libs/OptionsListUtils';
35+
import {doesPersonalDetailMatchSearchTerm} from '@libs/OptionsListUtils/searchMatchUtils';
4436
import type {OptionWithKey} from '@libs/OptionsListUtils/types';
4537
import type {OptionData} from '@libs/ReportUtils';
4638
import variables from '@styles/variables';
@@ -141,7 +133,7 @@ function useOptions(reportAttributesDerived: ReportAttributesDerivedValue['repor
141133
!!options.userToInvite,
142134
debouncedSearchTerm.trim(),
143135
countryCode,
144-
selectedOptions.some((participant) => getPersonalDetailSearchTerms(participant, currentUserAccountID).join(' ').toLowerCase?.().includes(cleanSearchTerm)),
136+
selectedOptions.some((participant) => doesPersonalDetailMatchSearchTerm(participant, currentUserAccountID, cleanSearchTerm)),
145137
);
146138

147139
useFocusEffect(() => {
@@ -247,19 +239,16 @@ function NewChatPage({ref}: NewChatPageProps) {
247239
const personalData = useCurrentUserPersonalDetails();
248240
const currentUserAccountID = personalData.accountID;
249241
const {top} = useSafeAreaInsets();
250-
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
251242
const [isSearchingForReports] = useOnyx(ONYXKEYS.RAM_ONLY_IS_SEARCHING_FOR_REPORTS);
252243
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
253244
const [betas] = useOnyx(ONYXKEYS.BETAS);
254245
const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
255-
const privateIsArchivedMap = usePrivateIsArchivedMap();
256246
const selectionListRef = useRef<SelectionListWithSectionsHandle | null>(null);
257247

258248
const [reportAttributesDerivedFull] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES);
259249

260250
const reportAttributesDerived = reportAttributesDerivedFull?.reports;
261251

262-
const allPersonalDetails = usePersonalDetails();
263252
const {singleExecution} = useSingleExecution();
264253

265254
useImperativeHandle(ref, () => ({
@@ -282,20 +271,12 @@ function NewChatPage({ref}: NewChatPageProps) {
282271

283272
const sections: Array<Section<OptionWithKey>> = [];
284273

285-
const formatResults = formatSectionsFromSearchTerm(
286-
debouncedSearchTerm,
287-
selectedOptions as OptionData[],
288-
recentReports,
289-
personalDetails,
290-
privateIsArchivedMap,
291-
currentUserAccountID,
292-
allPolicies,
293-
allPersonalDetails,
294-
undefined,
295-
undefined,
296-
reportAttributesDerived,
297-
);
298-
sections.push({...formatResults.section, title: undefined, sectionIndex: 0});
274+
const selectedSection =
275+
debouncedSearchTerm === ''
276+
? selectedOptions
277+
: selectedOptions.filter((participant) => doesPersonalDetailMatchSearchTerm(participant, currentUserAccountID, debouncedSearchTerm.trim().toLowerCase()));
278+
279+
sections.push({data: selectedSection, title: undefined, sectionIndex: 0});
299280

300281
sections.push({
301282
title: translate('common.recents'),

src/pages/iou/request/MoneyRequestAttendeeSelector.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ import {
2626
getFilteredRecentAttendees,
2727
getHeaderMessage,
2828
getParticipantsOption,
29-
getPersonalDetailSearchTerms,
3029
getPolicyExpenseReportOption,
3130
isCurrentUser,
3231
orderOptions,
3332
} from '@libs/OptionsListUtils';
33+
import {doesPersonalDetailMatchSearchTerm} from '@libs/OptionsListUtils/searchMatchUtils';
3434
import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
3535
import {isPaidGroupPolicy as isPaidGroupPolicyFn} from '@libs/PolicyUtils';
3636
import type {OptionData} from '@libs/ReportUtils';
@@ -272,7 +272,7 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde
272272
!!orderedAvailableOptions?.userToInvite,
273273
cleanSearchTerm,
274274
countryCode,
275-
attendees.some((attendee) => getPersonalDetailSearchTerms(attendee, currentUserAccountID).join(' ').toLowerCase().includes(cleanSearchTerm)),
275+
attendees.some((attendee) => doesPersonalDetailMatchSearchTerm(attendee, currentUserAccountID, cleanSearchTerm)),
276276
);
277277
sections = newSections;
278278
}

src/pages/iou/request/MoneyRequestParticipantsSelector.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ import goToSettings from '@libs/goToSettings';
3636
import {isMovingTransactionFromTrackExpense} from '@libs/IOUUtils';
3737
import Navigation from '@libs/Navigation/Navigation';
3838
import type {Option} from '@libs/OptionsListUtils';
39-
import {formatSectionsFromSearchTerm, getHeaderMessage, getParticipantsOption, getPersonalDetailSearchTerms, getPolicyExpenseReportOption, isCurrentUser} from '@libs/OptionsListUtils';
39+
import {formatSectionsFromSearchTerm, getHeaderMessage, getParticipantsOption, getPolicyExpenseReportOption, isCurrentUser} from '@libs/OptionsListUtils';
40+
import {doesPersonalDetailMatchSearchTerm} from '@libs/OptionsListUtils/searchMatchUtils';
4041
import type {OptionWithKey} from '@libs/OptionsListUtils/types';
4142
import {getActiveAdminWorkspaces, isPaidGroupPolicy as isPaidGroupPolicyUtil} from '@libs/PolicyUtils';
4243
import type {OptionData} from '@libs/ReportUtils';
@@ -265,7 +266,7 @@ function MoneyRequestParticipantsSelector({
265266
!!availableOptions?.userToInvite,
266267
debouncedSearchTerm.trim(),
267268
countryCode,
268-
participants.some((participant) => getPersonalDetailSearchTerms(participant, currentUserAccountID).join(' ').toLowerCase().includes(cleanSearchTerm)),
269+
participants.some((participant) => doesPersonalDetailMatchSearchTerm(participant, currentUserAccountID, cleanSearchTerm)),
269270
),
270271
// eslint-disable-next-line react-hooks/exhaustive-deps
271272
[

tests/unit/OptionsListUtilsTest.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,11 @@ import {
2424
filterWorkspaceChats,
2525
formatMemberForList,
2626
formatSectionsFromSearchTerm,
27-
getCurrentUserSearchTerms,
2827
getFilteredRecentAttendees,
2928
getIOUReportIDOfLastAction,
3029
getLastActorDisplayName,
3130
getLastActorDisplayNameFromLastVisibleActions,
3231
getLastMessageTextForReport,
33-
getPersonalDetailSearchTerms,
3432
getPolicyExpenseReportOption,
3533
getReportDisplayOption,
3634
getReportOption,
@@ -45,6 +43,7 @@ import {
4543
shouldShowLastActorDisplayName,
4644
sortAlphabetically,
4745
} from '@libs/OptionsListUtils';
46+
import {getCurrentUserSearchTerms, getPersonalDetailSearchTerms} from '@libs/OptionsListUtils/searchMatchUtils';
4847
import Parser from '@libs/Parser';
4948
import {
5049
getAddedCardFeedMessage,

0 commit comments

Comments
 (0)