From 8c563a634cb23c0754b431285eecd2004e8e6782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 22 Jul 2025 18:25:43 +0200 Subject: [PATCH 01/16] UserSelectPopup: memoize getValidOptions --- src/components/Search/FilterDropdowns/UserSelectPopup.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx index 56ecb8d4fcb4..3dee712a5dbf 100644 --- a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx +++ b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx @@ -12,6 +12,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; +import memoize from '@libs/memoize'; import type {Option, Section} from '@libs/OptionsListUtils'; import {filterAndOrderOptions, getValidOptions} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; @@ -33,6 +34,8 @@ type UserSelectPopupProps = { onChange: (value: string[]) => void; }; +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 1, monitoringName: 'UserSelectPopup.getValidOptions'}); + function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -65,7 +68,7 @@ function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps) // Get a list of all options/personal details and filter them by the current search term const listData = useMemo(() => { - const optionsList = getValidOptions( + const optionsList = memoizedGetValidOptions( { reports: options.reports, personalDetails: options.personalDetails, From 8325981d816bab6b0b537565c54978fc19cd6365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 23 Jul 2025 12:46:09 +0200 Subject: [PATCH 02/16] OptionListUtils: filterSelectedOptions --- src/libs/OptionsListUtils.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 87bff611fa77..17edc9f1e544 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2567,6 +2567,21 @@ function filterAndOrderOptions(options: Options, searchInputValue: string, confi }; } +/** + * Filter out selected options from personal details and recent reports + * @param options - The options to filter + * @param selectedOptions - The selected options to filter out. Keeping it as an array because it is unlikely to become big enough for Set to make a difference. + * @returns The filtered options + */ +function filterSelectedOptions(options: Options, selectedOptions: {accountID?: number}[]): Options { + const filteredOptions = { + ...options, + personalDetails: options.personalDetails.filter((detail) => !selectedOptions.some((selectedOption) => selectedOption.accountID === detail.accountID)), + recentReports: options.recentReports.filter((report) => !selectedOptions.some((selectedOption) => selectedOption.accountID === report.accountID)), + }; + return filteredOptions; +} + function sortAlphabetically>, TKey extends keyof T>(items: T[], key: TKey): T[] { return items.sort((a, b) => (a[key] ?? '').toLowerCase().localeCompare((b[key] ?? '').toLowerCase())); } @@ -2676,6 +2691,7 @@ export { shallowOptionsListCompare, optionsOrderBy, recentReportComparator, + filterSelectedOptions, }; export type {Section, SectionBase, MemberForList, Options, OptionList, SearchOption, Option, OptionTree, ReportAndPersonalDetailOptions}; From e9d797ad40275f82e330a5cb146558084473a36a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 23 Jul 2025 12:46:54 +0200 Subject: [PATCH 03/16] NewChatPage: skip getValidOptions on change --- src/pages/NewChatPage.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 06b83b0d9e5b..f87fc6e7057b 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -27,6 +27,7 @@ import Navigation from '@libs/Navigation/Navigation'; import type {Option, Section} from '@libs/OptionsListUtils'; import { filterAndOrderOptions, + filterSelectedOptions, formatSectionsFromSearchTerm, getFirstKeyForList, getHeaderMessage, @@ -68,21 +69,22 @@ function useOptions() { }, { betas: betas ?? [], - selectedOptions, includeSelfDM: true, }, ); return filteredOptions; - }, [betas, listOptions.personalDetails, listOptions.reports, selectedOptions]); + }, [betas, listOptions.personalDetails, listOptions.reports]); + + const unselectedOptions = useMemo(() => filterSelectedOptions(defaultOptions, selectedOptions), [defaultOptions, selectedOptions]); const options = useMemo(() => { - const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchTerm, { + const filteredOptions = filterAndOrderOptions(unselectedOptions, debouncedSearchTerm, { selectedOptions, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, }); return filteredOptions; - }, [debouncedSearchTerm, defaultOptions, selectedOptions]); + }, [debouncedSearchTerm, unselectedOptions, selectedOptions]); const cleanSearchTerm = useMemo(() => debouncedSearchTerm.trim().toLowerCase(), [debouncedSearchTerm]); const headerMessage = useMemo(() => { return getHeaderMessage( From 968819bd50f34816a273b0144f4a79fd3afe0717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 23 Jul 2025 12:58:08 +0200 Subject: [PATCH 04/16] UserSelectPopup: fix memoization --- src/components/Search/FilterDropdowns/UserSelectPopup.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx index 3dee712a5dbf..08cd436d6f1a 100644 --- a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx +++ b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx @@ -74,7 +74,6 @@ function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps) personalDetails: options.personalDetails, }, { - selectedOptions, excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, includeSelectedOptions: true, includeCurrentUser: true, From 9baef3e337bbac2cb72fd0c8f72b17cf42f462ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 23 Jul 2025 22:42:23 +0200 Subject: [PATCH 05/16] filterSelectedOptions: changeselectedOptions to Set --- src/libs/OptionsListUtils.ts | 6 +++--- src/pages/NewChatPage.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 17edc9f1e544..cd97e844a862 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2573,11 +2573,11 @@ function filterAndOrderOptions(options: Options, searchInputValue: string, confi * @param selectedOptions - The selected options to filter out. Keeping it as an array because it is unlikely to become big enough for Set to make a difference. * @returns The filtered options */ -function filterSelectedOptions(options: Options, selectedOptions: {accountID?: number}[]): Options { +function filterSelectedOptions(options: Options, selectedOptions: Set): Options { const filteredOptions = { ...options, - personalDetails: options.personalDetails.filter((detail) => !selectedOptions.some((selectedOption) => selectedOption.accountID === detail.accountID)), - recentReports: options.recentReports.filter((report) => !selectedOptions.some((selectedOption) => selectedOption.accountID === report.accountID)), + personalDetails: options.personalDetails.filter(({accountID}) => !selectedOptions.has(accountID)), + recentReports: options.recentReports.filter(({accountID}) => !selectedOptions.has(accountID)), }; return filteredOptions; } diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index f87fc6e7057b..f7dc06d8e39b 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -75,7 +75,7 @@ function useOptions() { return filteredOptions; }, [betas, listOptions.personalDetails, listOptions.reports]); - const unselectedOptions = useMemo(() => filterSelectedOptions(defaultOptions, selectedOptions), [defaultOptions, selectedOptions]); + const unselectedOptions = useMemo(() => filterSelectedOptions(defaultOptions, new Set(selectedOptions.map(({accountID}) => accountID))), [defaultOptions, selectedOptions]); const options = useMemo(() => { const filteredOptions = filterAndOrderOptions(unselectedOptions, debouncedSearchTerm, { From e0014be333cd7e0ac18667ae09746bddfdd4e68f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 24 Jul 2025 11:26:17 +0200 Subject: [PATCH 06/16] TaskAssigneeSelectorModal: memoize getValidOptions --- src/pages/tasks/TaskAssigneeSelectorModal.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index d3d633875197..0fcb9a0bc536 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -23,6 +23,7 @@ import {searchInServer} from '@libs/actions/Report'; import {canModifyTask, editTaskAssignee, setAssigneeValue} from '@libs/actions/Task'; import {READ_COMMANDS} from '@libs/API/types'; import HttpUtils from '@libs/HttpUtils'; +import memoize from '@libs/memoize'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import {filterAndOrderOptions, getHeaderMessage, getValidOptions, isCurrentUser} from '@libs/OptionsListUtils'; @@ -34,6 +35,8 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Report} from '@src/types/onyx'; +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 1, monitoringName: 'TaskAssigneeSelectorModal.getValidOptions'}); + function useOptions() { const betas = useBetas(); const [isLoading, setIsLoading] = useState(true); @@ -41,7 +44,7 @@ function useOptions() { const {options: optionsList, areOptionsInitialized} = useOptionsList(); const defaultOptions = useMemo(() => { - const {recentReports, personalDetails, userToInvite, currentUserOption} = getValidOptions( + const {recentReports, personalDetails, userToInvite, currentUserOption} = memoizedGetValidOptions( { reports: optionsList.reports, personalDetails: optionsList.personalDetails, From c22ecddcddec41fa5abed7e5529f6ce9a0f88495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 24 Jul 2025 11:28:02 +0200 Subject: [PATCH 07/16] NewChatPage: memoize getValidOptions --- src/pages/NewChatPage.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index f7dc06d8e39b..8a7bdc84095f 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -23,6 +23,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {navigateToAndOpenReport, searchInServer, setGroupDraft} from '@libs/actions/Report'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; +import memoize from '@libs/memoize'; import Navigation from '@libs/Navigation/Navigation'; import type {Option, Section} from '@libs/OptionsListUtils'; import { @@ -50,6 +51,8 @@ type SelectedOption = ListItem & reportID?: string; }; +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 1, monitoringName: 'NewChatPage.getValidOptions'}); + function useOptions() { const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const [selectedOptions, setSelectedOptions] = useState([]); @@ -62,7 +65,7 @@ function useOptions() { }); const defaultOptions = useMemo(() => { - const filteredOptions = getValidOptions( + const filteredOptions = memoizedGetValidOptions( { reports: listOptions.reports ?? [], personalDetails: listOptions.personalDetails ?? [], From e3b7373478870a9877a49cff0041c6931356bb3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 24 Jul 2025 11:43:29 +0200 Subject: [PATCH 08/16] SearchFiltersParticipantsSelector: memoize --- src/components/Search/SearchFiltersParticipantsSelector.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index ea26d2134a9a..001fb9434127 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -7,6 +7,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useScreenWrapperTransitionStatus from '@hooks/useScreenWrapperTransitionStatus'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; +import memoize from '@libs/memoize'; import {filterAndOrderOptions, formatSectionsFromSearchTerm, getValidOptions} from '@libs/OptionsListUtils'; import type {Option, Section} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; @@ -35,6 +36,8 @@ type SearchFiltersParticipantsSelectorProps = { onFiltersUpdate: (accountIDs: string[]) => void; }; +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 1, monitoringName: 'SearchFiltersParticipantsSelector.getValidOptions'}); + function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: SearchFiltersParticipantsSelectorProps) { const {translate} = useLocalize(); const personalDetails = usePersonalDetails(); @@ -53,7 +56,7 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: return defaultListOptions; } - return getValidOptions( + const result = memoizedGetValidOptions( { reports: options.reports, personalDetails: options.personalDetails, @@ -63,6 +66,7 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, }, ); + return result; }, [areOptionsInitialized, options.personalDetails, options.reports, selectedOptions]); const chatOptions = useMemo(() => { From e7dc6e9809ae8725816056ea80cef21e626e4283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 24 Jul 2025 11:43:37 +0200 Subject: [PATCH 09/16] MoneyRequestAccountantSelector: memoize --- src/pages/iou/request/MoneyRequestAccountantSelector.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/MoneyRequestAccountantSelector.tsx b/src/pages/iou/request/MoneyRequestAccountantSelector.tsx index 107a2de8a306..95e85d71aab0 100644 --- a/src/pages/iou/request/MoneyRequestAccountantSelector.tsx +++ b/src/pages/iou/request/MoneyRequestAccountantSelector.tsx @@ -12,6 +12,7 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useScreenWrapperTransitionStatus from '@hooks/useScreenWrapperTransitionStatus'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; +import memoize from '@libs/memoize'; import type {Section} from '@libs/OptionsListUtils'; import { filterAndOrderOptions, @@ -30,6 +31,8 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Accountant} from '@src/types/onyx/IOU'; +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 1, monitoringName: 'MoneyRequestAccountantSelector.getValidOptions'}); + type MoneyRequestAccountantSelectorProps = { /** Callback to request parent modal to go to next step */ onFinish: (value?: string) => void; @@ -66,7 +69,7 @@ function MoneyRequestAccountantSelector({onFinish, onAccountantSelected, iouType getEmptyOptions(); } - const optionList = getValidOptions( + const optionList = memoizedGetValidOptions( { reports: options.reports, personalDetails: options.personalDetails, From 544156d5762178b90a0b9c15739c3fc89e171fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 24 Jul 2025 11:43:47 +0200 Subject: [PATCH 10/16] MoneyRequestParticipantsSelector: memoize --- src/pages/iou/request/MoneyRequestParticipantsSelector.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 9783c6423e4a..8f01594a166e 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -35,6 +35,7 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import getPlatform from '@libs/getPlatform'; import goToSettings from '@libs/goToSettings'; import {isMovingTransactionFromTrackExpense} from '@libs/IOUUtils'; +import memoize from '@libs/memoize'; import Navigation from '@libs/Navigation/Navigation'; import type {Option, SearchOption, Section} from '@libs/OptionsListUtils'; import { @@ -63,6 +64,8 @@ import type {Participant} from '@src/types/onyx/IOU'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import ImportContactButton from './ImportContactButton'; +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 1, monitoringName: 'MoneyRequestParticipantsSelector.getValidOptions'}); + type MoneyRequestParticipantsSelectorProps = { /** Callback to request parent modal to go to next step, which should be split */ onFinish?: (value?: string) => void; @@ -168,7 +171,7 @@ function MoneyRequestParticipantsSelector( }; } - const optionList = getValidOptions( + const optionList = memoizedGetValidOptions( { reports: options.reports, personalDetails: options.personalDetails.concat(contacts), From cc899800c439cc3bd674560b915583f9ea148895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 24 Jul 2025 11:43:58 +0200 Subject: [PATCH 11/16] AddDelegatePage: memoize --- src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx index 84e606a931aa..52fb5568a984 100644 --- a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx +++ b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx @@ -12,6 +12,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {searchInServer} from '@libs/actions/Report'; +import memoize from '@libs/memoize'; import Navigation from '@libs/Navigation/Navigation'; import {filterAndOrderOptions, getHeaderMessage, getValidOptions} from '@libs/OptionsListUtils'; import CONST from '@src/CONST'; @@ -19,6 +20,8 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Participant} from '@src/types/onyx/IOU'; +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 1, monitoringName: 'AddDelegatePage.getValidOptions'}); + function useOptions() { const betas = useBetas(); const [isLoading, setIsLoading] = useState(true); @@ -39,7 +42,7 @@ function useOptions() { ); const defaultOptions = useMemo(() => { - const {recentReports, personalDetails, userToInvite, currentUserOption} = getValidOptions( + const {recentReports, personalDetails, userToInvite, currentUserOption} = memoizedGetValidOptions( { reports: optionsList.reports, personalDetails: optionsList.personalDetails, From 783f9fe713baba54e96aab1ae7f94cd749d47434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 24 Jul 2025 11:45:42 +0200 Subject: [PATCH 12/16] extend cache size to 5 --- src/components/Search/FilterDropdowns/UserSelectPopup.tsx | 2 +- src/components/Search/SearchFiltersParticipantsSelector.tsx | 2 +- src/pages/NewChatPage.tsx | 2 +- src/pages/iou/request/MoneyRequestAccountantSelector.tsx | 2 +- src/pages/iou/request/MoneyRequestParticipantsSelector.tsx | 2 +- src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx | 2 +- src/pages/tasks/TaskAssigneeSelectorModal.tsx | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx index 08cd436d6f1a..4747436ab43e 100644 --- a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx +++ b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx @@ -34,7 +34,7 @@ type UserSelectPopupProps = { onChange: (value: string[]) => void; }; -const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 1, monitoringName: 'UserSelectPopup.getValidOptions'}); +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'UserSelectPopup.getValidOptions'}); function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps) { const styles = useThemeStyles(); diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index 001fb9434127..2c878886a007 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -36,7 +36,7 @@ type SearchFiltersParticipantsSelectorProps = { onFiltersUpdate: (accountIDs: string[]) => void; }; -const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 1, monitoringName: 'SearchFiltersParticipantsSelector.getValidOptions'}); +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'SearchFiltersParticipantsSelector.getValidOptions'}); function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: SearchFiltersParticipantsSelectorProps) { const {translate} = useLocalize(); diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 8a7bdc84095f..99e105ef99fb 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -51,7 +51,7 @@ type SelectedOption = ListItem & reportID?: string; }; -const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 1, monitoringName: 'NewChatPage.getValidOptions'}); +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'NewChatPage.getValidOptions'}); function useOptions() { const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); diff --git a/src/pages/iou/request/MoneyRequestAccountantSelector.tsx b/src/pages/iou/request/MoneyRequestAccountantSelector.tsx index 95e85d71aab0..9e36005750ea 100644 --- a/src/pages/iou/request/MoneyRequestAccountantSelector.tsx +++ b/src/pages/iou/request/MoneyRequestAccountantSelector.tsx @@ -31,7 +31,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Accountant} from '@src/types/onyx/IOU'; -const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 1, monitoringName: 'MoneyRequestAccountantSelector.getValidOptions'}); +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'MoneyRequestAccountantSelector.getValidOptions'}); type MoneyRequestAccountantSelectorProps = { /** Callback to request parent modal to go to next step */ diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 8f01594a166e..bcd8cacbb92e 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -64,7 +64,7 @@ import type {Participant} from '@src/types/onyx/IOU'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import ImportContactButton from './ImportContactButton'; -const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 1, monitoringName: 'MoneyRequestParticipantsSelector.getValidOptions'}); +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'MoneyRequestParticipantsSelector.getValidOptions'}); type MoneyRequestParticipantsSelectorProps = { /** Callback to request parent modal to go to next step, which should be split */ diff --git a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx index 52fb5568a984..54828fe79d23 100644 --- a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx +++ b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx @@ -20,7 +20,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Participant} from '@src/types/onyx/IOU'; -const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 1, monitoringName: 'AddDelegatePage.getValidOptions'}); +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'AddDelegatePage.getValidOptions'}); function useOptions() { const betas = useBetas(); diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index 0fcb9a0bc536..d347ba1d28a6 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -35,7 +35,7 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Report} from '@src/types/onyx'; -const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 1, monitoringName: 'TaskAssigneeSelectorModal.getValidOptions'}); +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'TaskAssigneeSelectorModal.getValidOptions'}); function useOptions() { const betas = useBetas(); From ad62bbeb8769947d86cdfe01c7cf2c5aa4b4bb92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Thu, 24 Jul 2025 14:01:06 +0200 Subject: [PATCH 13/16] useSearchParticipantsOptions: memoize getValidOptions --- src/hooks/useSearchParticipantsOptions.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/hooks/useSearchParticipantsOptions.ts b/src/hooks/useSearchParticipantsOptions.ts index 659ba4a0dc53..668039965a38 100644 --- a/src/hooks/useSearchParticipantsOptions.ts +++ b/src/hooks/useSearchParticipantsOptions.ts @@ -1,7 +1,8 @@ import {useMemo} from 'react'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; import {useOptionsList} from '@components/OptionListContextProvider'; -import {filterAndOrderOptions, formatSectionsFromSearchTerm, getValidOptions} from '@libs/OptionsListUtils'; +import memoize from '@libs/memoize'; +import {filterAndOrderOptions, filterSelectedOptions, formatSectionsFromSearchTerm, getValidOptions} from '@libs/OptionsListUtils'; import type {Section} from '@libs/OptionsListUtils'; import {getDisplayNameForParticipant} from '@libs/ReportUtils'; import type {OptionData} from '@libs/ReportUtils'; @@ -16,6 +17,8 @@ const defaultListOptions = { headerMessage: '', }; +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'useSearchParticipantsOptions.getValidOptions'}); + function useSearchParticipantsOptions({selectedOptions, cleanSearchTerm, shouldInitialize = true}: {selectedOptions: OptionData[]; cleanSearchTerm: string; shouldInitialize?: boolean}) { const {options, areOptionsInitialized} = useOptionsList({shouldInitialize}); const isReady = !!areOptionsInitialized; @@ -27,26 +30,29 @@ function useSearchParticipantsOptions({selectedOptions, cleanSearchTerm, shouldI return defaultListOptions; } - return getValidOptions( + return memoizedGetValidOptions( { reports: options.reports, personalDetails: options.personalDetails, }, { - selectedOptions, excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, }, ); - }, [isReady, options.personalDetails, options.reports, selectedOptions]); + }, [isReady, options.personalDetails, options.reports]); + + const unselectedOptions = useMemo(() => { + return filterSelectedOptions(defaultOptions, new Set(selectedOptions.map(({accountID}) => accountID))); + }, [defaultOptions, selectedOptions]); const chatOptions = useMemo(() => { - return filterAndOrderOptions(defaultOptions, cleanSearchTerm, { + return filterAndOrderOptions(unselectedOptions, cleanSearchTerm, { selectedOptions, excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, canInviteUser: false, }); - }, [defaultOptions, cleanSearchTerm, selectedOptions]); + }, [unselectedOptions, cleanSearchTerm, selectedOptions]); const {sections, headerMessage} = useMemo<{ sections: Section[]; From 58ab5308368987a6d84738e7b8d8ca1c2f9599c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 25 Jul 2025 13:54:43 +0200 Subject: [PATCH 14/16] filterSelectedOptions: fix description --- src/libs/OptionsListUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 49a732eddb76..1d9d53c6c796 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2574,7 +2574,7 @@ function filterAndOrderOptions(options: Options, searchInputValue: string, confi /** * Filter out selected options from personal details and recent reports * @param options - The options to filter - * @param selectedOptions - The selected options to filter out. Keeping it as an array because it is unlikely to become big enough for Set to make a difference. + * @param selectedOptions - The selected options to filter out. * @returns The filtered options */ function filterSelectedOptions(options: Options, selectedOptions: Set): Options { From 96b666c8ed96ffeed9b2bee1f88d522f7d01ee5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 25 Jul 2025 14:52:47 +0200 Subject: [PATCH 15/16] Main merge conflicts: SearchFiltersParticipantsSelector --- .../SearchFiltersParticipantsSelector.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index ea26d2134a9a..ca447b7e26ab 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -7,7 +7,8 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useScreenWrapperTransitionStatus from '@hooks/useScreenWrapperTransitionStatus'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; -import {filterAndOrderOptions, formatSectionsFromSearchTerm, getValidOptions} from '@libs/OptionsListUtils'; +import memoize from '@libs/memoize'; +import {filterAndOrderOptions, filterSelectedOptions, formatSectionsFromSearchTerm, getValidOptions} from '@libs/OptionsListUtils'; import type {Option, Section} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; import {getDisplayNameForParticipant} from '@libs/ReportUtils'; @@ -25,6 +26,8 @@ const defaultListOptions = { headerMessage: '', }; +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'SearchFiltersParticipantsSelector.getValidOptions'}); + function getSelectedOptionData(option: Option): OptionData { // eslint-disable-next-line rulesdir/no-default-id-values return {...option, selected: true, reportID: option.reportID ?? '-1'}; @@ -53,26 +56,29 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: return defaultListOptions; } - return getValidOptions( + return memoizedGetValidOptions( { reports: options.reports, personalDetails: options.personalDetails, }, { - selectedOptions, excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, }, ); - }, [areOptionsInitialized, options.personalDetails, options.reports, selectedOptions]); + }, [areOptionsInitialized, options.personalDetails, options.reports]); + + const unselectedOptions = useMemo(() => { + return filterSelectedOptions(defaultOptions, new Set(selectedOptions.map((option) => option.accountID))); + }, [defaultOptions, selectedOptions]); const chatOptions = useMemo(() => { - return filterAndOrderOptions(defaultOptions, cleanSearchTerm, { + return filterAndOrderOptions(unselectedOptions, cleanSearchTerm, { selectedOptions, excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, canInviteUser: false, }); - }, [defaultOptions, cleanSearchTerm, selectedOptions]); + }, [unselectedOptions, cleanSearchTerm, selectedOptions]); const {sections, headerMessage} = useMemo(() => { const newSections: Section[] = []; From efe37e5febcc18c6706a56bda87db137ce08d19d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 25 Jul 2025 18:56:43 +0200 Subject: [PATCH 16/16] UserSelectPopup: fix memoization --- .../FilterDropdowns/UserSelectPopup.tsx | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx index 56ecb8d4fcb4..36111245a3f0 100644 --- a/src/components/Search/FilterDropdowns/UserSelectPopup.tsx +++ b/src/components/Search/FilterDropdowns/UserSelectPopup.tsx @@ -12,6 +12,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; +import memoize from '@libs/memoize'; import type {Option, Section} from '@libs/OptionsListUtils'; import {filterAndOrderOptions, getValidOptions} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; @@ -22,6 +23,8 @@ function getSelectedOptionData(option: Option) { return {...option, reportID: `${option.reportID}`, selected: true}; } +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'UserSelectPopup.getValidOptions'}); + type UserSelectPopupProps = { /** The currently selected users */ value: string[]; @@ -63,28 +66,29 @@ function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps) const cleanSearchTerm = searchTerm.trim().toLowerCase(); - // Get a list of all options/personal details and filter them by the current search term - const listData = useMemo(() => { - const optionsList = getValidOptions( + const optionsList = useMemo(() => { + return memoizedGetValidOptions( { reports: options.reports, personalDetails: options.personalDetails, }, { - selectedOptions, excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, - includeSelectedOptions: true, includeCurrentUser: true, }, ); + }, [options.reports, options.personalDetails]); - const {personalDetails: filteredOptionsList, recentReports} = filterAndOrderOptions(optionsList, cleanSearchTerm, { + const filteredOptions = useMemo(() => { + return filterAndOrderOptions(optionsList, cleanSearchTerm, { excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, canInviteUser: false, }); + }, [optionsList, cleanSearchTerm]); - const personalDetailList = filteredOptionsList + const listData = useMemo(() => { + const personalDetailList = filteredOptions.personalDetails .map((participant) => ({ ...participant, isSelected: selectedOptions.some((selectedOption) => selectedOption.accountID === participant.accountID), @@ -100,8 +104,16 @@ function UserSelectPopup({value, closeOverlay, onChange}: UserSelectPopupProps) return 0; }); - return [...(personalDetailList ?? []), ...(recentReports ?? [])]; - }, [cleanSearchTerm, options.personalDetails, options.reports, selectedOptions, accountID]); + const recentReportsList = filteredOptions.recentReports.map((report) => { + const isSelected = selectedOptions.some((selectedOption) => selectedOption.reportID === report.reportID); + return { + ...report, + isSelected, + }; + }); + + return [...personalDetailList, ...recentReportsList]; + }, [filteredOptions, selectedOptions, accountID]); const {sections, headerMessage} = useMemo(() => { const newSections: Section[] = [