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[] = [ 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[] = []; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 7001c9fbcd6c..2b9ac2de089b 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2572,6 +2572,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. + * @returns The filtered options + */ +function filterSelectedOptions(options: Options, selectedOptions: Set): Options { + const filteredOptions = { + ...options, + personalDetails: options.personalDetails.filter(({accountID}) => !selectedOptions.has(accountID)), + recentReports: options.recentReports.filter(({accountID}) => !selectedOptions.has(accountID)), + }; + return filteredOptions; +} + function sortAlphabetically>, TKey extends keyof T>(items: T[], key: TKey): T[] { return items.sort((a, b) => localeCompare(a[key]?.toLowerCase() ?? '', b[key]?.toLowerCase() ?? '')); } @@ -2681,6 +2696,7 @@ export { shallowOptionsListCompare, optionsOrderBy, recentReportComparator, + filterSelectedOptions, }; export type {Section, SectionBase, MemberForList, Options, OptionList, SearchOption, Option, OptionTree, ReportAndPersonalDetailOptions}; diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 06b83b0d9e5b..99e105ef99fb 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -23,10 +23,12 @@ 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 { filterAndOrderOptions, + filterSelectedOptions, formatSectionsFromSearchTerm, getFirstKeyForList, getHeaderMessage, @@ -49,6 +51,8 @@ type SelectedOption = ListItem & reportID?: string; }; +const memoizedGetValidOptions = memoize(getValidOptions, {maxSize: 5, monitoringName: 'NewChatPage.getValidOptions'}); + function useOptions() { const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const [selectedOptions, setSelectedOptions] = useState([]); @@ -61,28 +65,29 @@ function useOptions() { }); const defaultOptions = useMemo(() => { - const filteredOptions = getValidOptions( + const filteredOptions = memoizedGetValidOptions( { reports: listOptions.reports ?? [], personalDetails: listOptions.personalDetails ?? [], }, { betas: betas ?? [], - selectedOptions, includeSelfDM: true, }, ); return filteredOptions; - }, [betas, listOptions.personalDetails, listOptions.reports, selectedOptions]); + }, [betas, listOptions.personalDetails, listOptions.reports]); + + const unselectedOptions = useMemo(() => filterSelectedOptions(defaultOptions, new Set(selectedOptions.map(({accountID}) => accountID))), [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( diff --git a/src/pages/iou/request/MoneyRequestAccountantSelector.tsx b/src/pages/iou/request/MoneyRequestAccountantSelector.tsx index 107a2de8a306..9e36005750ea 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: 5, 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, diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 9783c6423e4a..bcd8cacbb92e 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: 5, 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), diff --git a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx index 84e606a931aa..54828fe79d23 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: 5, 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, diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index d3d633875197..d347ba1d28a6 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: 5, 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,