From fd9f06c6d8ec90c60b14b304090578675b6b68d3 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Fri, 4 Jul 2025 00:42:00 +0530 Subject: [PATCH 01/50] Stop scrolling to top when selecting an item --- src/components/SelectionList/BaseSelectionList.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index f8116710869b..d2e8aedf5e1e 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -421,11 +421,6 @@ function BaseSelectionList( } // In single-selection lists we don't care about updating the focused index, because the list is closed after selecting an item if (canSelectMultiple) { - if (sections.length > 1 && !isItemSelected(item)) { - // If we're selecting an item, scroll to it's position at the top, so we can see it - scrollToIndex(0, true); - } - if (shouldShowTextInput) { clearInputAfterSelect(); } else if (isSmallScreenWidth) { From e521fc0d11e8e17d5b2d6c2828b47afccc0411b1 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 23 Jul 2025 22:04:20 +0530 Subject: [PATCH 02/50] combine selected and non selected workspaces in a single section --- src/hooks/useWorkspaceList.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/hooks/useWorkspaceList.ts b/src/hooks/useWorkspaceList.ts index 6cff4900a53c..0c3cc26f5206 100644 --- a/src/hooks/useWorkspaceList.ts +++ b/src/hooks/useWorkspaceList.ts @@ -2,6 +2,7 @@ import {useMemo} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; import type {ListItem, SectionListDataType} from '@components/SelectionList/types'; +import localeCompare from '@libs/LocaleCompare'; import {isPolicyAdmin, shouldShowPolicy, sortWorkspacesBySelected} from '@libs/PolicyUtils'; import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; import tokenizedSearch from '@libs/tokenizedSearch'; @@ -59,10 +60,7 @@ function useWorkspaceList({policies, currentUserLogin, selectedPolicyIDs, search }, [policies, shouldShowPendingDeletePolicy, currentUserLogin, additionalFilter, selectedPolicyIDs]); const filteredAndSortedUserWorkspaces = useMemo( - () => - tokenizedSearch(usersWorkspaces, searchTerm, (policy) => [policy.text]).sort((policy1, policy2) => - sortWorkspacesBySelected({policyID: policy1.policyID, name: policy1.text}, {policyID: policy2.policyID, name: policy2.text}, selectedPolicyIDs), - ), + () => tokenizedSearch(usersWorkspaces, searchTerm, (policy) => [policy.text]).sort((policy1, policy2) => localeCompare(policy1.text, policy2.text)), [searchTerm, usersWorkspaces, selectedPolicyIDs], ); From 1bf11125330bff6e2f2253beb8178fbd94a6bad0 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 23 Jul 2025 22:04:29 +0530 Subject: [PATCH 03/50] combine selected and non selected items in a single section --- .../Search/SearchMultipleSelectionPicker.tsx | 31 +++++-------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index b26c2a62fcef..640d1812b7c2 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -33,41 +33,24 @@ function SearchMultipleSelectionPicker({items, initiallySelectedItems, pickerTit }, [initiallySelectedItems]); const {sections, noResultsFound} = useMemo(() => { - const selectedItemsSection = selectedItems - .filter((item) => item?.name.toLowerCase().includes(debouncedSearchTerm?.toLowerCase())) - .sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString())) + const itemsSection = items + .filter((item) => item?.name?.toLowerCase().includes(debouncedSearchTerm?.toLowerCase())) + .sort((a, b) => sortOptionsWithEmptyValue(a.value as string, b.value as string)) .map((item) => ({ text: item.name, keyForList: item.name, - isSelected: true, + isSelected: selectedItems.some((selectedItem) => selectedItem.value === item.value), value: item.value, })); - const remainingItemsSection = items - .filter( - (item) => - !selectedItems.some((selectedItem) => selectedItem.value.toString() === item.value.toString()) && item?.name?.toLowerCase().includes(debouncedSearchTerm?.toLowerCase()), - ) - .sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString())) - .map((item) => ({ - text: item.name, - keyForList: item.name, - isSelected: false, - value: item.value, - })); - const isEmpty = !selectedItemsSection.length && !remainingItemsSection.length; + const isEmpty = !itemsSection.length; return { sections: isEmpty ? [] : [ - { - title: undefined, - data: selectedItemsSection, - shouldShow: selectedItemsSection.length > 0, - }, { title: pickerTitle, - data: remainingItemsSection, - shouldShow: remainingItemsSection.length > 0, + data: itemsSection, + shouldShow: itemsSection.length > 0, }, ], noResultsFound: isEmpty, From 6fda28977e41ec422ebfcc0216483cf6e959ba57 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Mon, 11 Aug 2025 21:28:05 +0530 Subject: [PATCH 04/50] unify selected and non-selected options --- src/pages/workspace/WorkspaceInvitePage.tsx | 26 +++------------------ 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index fa8647289204..e0abbc7a1961 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -170,30 +170,9 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { return []; } - // Filter all options that is a part of the search term or in the personal details - let filterSelectedOptions = selectedOptions; - if (debouncedSearchTerm !== '') { - filterSelectedOptions = selectedOptions.filter((option) => { - const accountID = option.accountID; - const isOptionInPersonalDetails = Object.values(personalDetails).some((personalDetail) => personalDetail.accountID === accountID); - - const searchValue = getSearchValueForPhoneOrEmail(debouncedSearchTerm); - - const isPartOfSearchTerm = !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); - return isPartOfSearchTerm || isOptionInPersonalDetails; - }); - } - - sectionsArr.push({ - title: undefined, - data: filterSelectedOptions, - shouldShow: true, - }); - - // Filtering out selected users from the search results const selectedLogins = selectedOptions.map(({login}) => login); - const personalDetailsWithoutSelected = Object.values(personalDetails).filter(({login}) => !selectedLogins.some((selectedLogin) => selectedLogin === login)); - const personalDetailsFormatted = personalDetailsWithoutSelected.map((item) => formatMemberForList(item)); + const personalDetailsModified = Object.values(personalDetails).map((item) => selectedLogins.some((selectedLogin)=> item.login === selectedLogin) ? {...item, isSelected : true} : item); + const personalDetailsFormatted = personalDetailsModified.map((item) => formatMemberForList(item)); sectionsArr.push({ title: translate('common.contacts'), @@ -341,3 +320,4 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { WorkspaceInvitePage.displayName = 'WorkspaceInvitePage'; export default withNavigationTransitionEnd(withPolicyAndFullscreenLoading(WorkspaceInvitePage)); + From e0477fff6b6ab15d693cb3a5103970e5e219e7a2 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Mon, 11 Aug 2025 21:35:16 +0530 Subject: [PATCH 05/50] unify selected and non-selected options --- src/pages/RoomInvitePage.tsx | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 6499e08d5810..5e49838557d4 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -122,29 +122,9 @@ function RoomInvitePage({ return []; } - // Filter all options that is a part of the search term or in the personal details - let filterSelectedOptions = selectedOptions; - if (debouncedSearchTerm !== '') { - filterSelectedOptions = selectedOptions.filter((option) => { - const accountID = option?.accountID; - const isOptionInPersonalDetails = personalDetails ? personalDetails.some((personalDetail) => accountID && personalDetail?.accountID === accountID) : false; - const parsedPhoneNumber = parsePhoneNumber(appendCountryCode(Str.removeSMSDomain(debouncedSearchTerm))); - const searchValue = parsedPhoneNumber.possible && parsedPhoneNumber.number ? parsedPhoneNumber.number.e164 : debouncedSearchTerm.toLowerCase(); - const isPartOfSearchTerm = (option.text?.toLowerCase() ?? '').includes(searchValue) || (option.login?.toLowerCase() ?? '').includes(searchValue); - return isPartOfSearchTerm || isOptionInPersonalDetails; - }); - } - const filterSelectedOptionsFormatted = filterSelectedOptions.map((selectedOption) => formatMemberForList(selectedOption)); - - sectionsArr.push({ - title: undefined, - data: filterSelectedOptionsFormatted, - }); - - // Filtering out selected users from the search results const selectedLogins = selectedOptions.map(({login}) => login); - const personalDetailsWithoutSelected = personalDetails ? personalDetails.filter(({login}) => !selectedLogins.includes(login)) : []; - const personalDetailsFormatted = personalDetailsWithoutSelected.map((personalDetail) => formatMemberForList(personalDetail)); + const personalDetailsModified = Object.values(personalDetails).map((item) => selectedLogins.some((selectedLogin)=> item.login === selectedLogin) ? {...item, isSelected : true} : item); + const personalDetailsFormatted = personalDetailsModified.map((personalDetail) => formatMemberForList(personalDetail)); const hasUnselectedUserToInvite = userToInvite && !selectedLogins.includes(userToInvite.login); sectionsArr.push({ From 51cc4eb0de9f5cd2c02e1965e194cb41dcabc320 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Mon, 11 Aug 2025 22:06:50 +0530 Subject: [PATCH 06/50] unify selected and non-selected options --- src/pages/InviteReportParticipantsPage.tsx | 27 ++++------------------ 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/src/pages/InviteReportParticipantsPage.tsx b/src/pages/InviteReportParticipantsPage.tsx index bbe83f58ee16..5c1b9f485a3c 100644 --- a/src/pages/InviteReportParticipantsPage.tsx +++ b/src/pages/InviteReportParticipantsPage.tsx @@ -114,30 +114,11 @@ function InviteReportParticipantsPage({betas, report, didScreenTransitionEnd}: I return []; } - // Filter all options that is a part of the search term or in the personal details - let filterSelectedOptions = selectedOptions; - if (debouncedSearchTerm !== '') { - const processedSearchValue = getSearchValueForPhoneOrEmail(debouncedSearchTerm); - filterSelectedOptions = tokenizedSearch(selectedOptions, processedSearchValue, (option) => [option.text ?? '', option.login ?? '']).filter((option) => { - const accountID = option?.accountID; - const isOptionInPersonalDetails = inviteOptions.personalDetails.some((personalDetail) => accountID && personalDetail?.accountID === accountID); - const isPartOfSearchTerm = !!option.text?.toLowerCase().includes(processedSearchValue) || !!option.login?.toLowerCase().includes(processedSearchValue); - return isPartOfSearchTerm || isOptionInPersonalDetails; - }); - } - const filterSelectedOptionsFormatted = filterSelectedOptions.map((selectedOption) => formatMemberForList(selectedOption)); - - sectionsArr.push({ - title: undefined, - data: filterSelectedOptionsFormatted, - }); - - // Filtering out selected users from the search results const selectedLogins = selectedOptions.map(({login}) => login); - const recentReportsWithoutSelected = inviteOptions.recentReports.filter(({login}) => !selectedLogins.includes(login)); - const recentReportsFormatted = recentReportsWithoutSelected.map((reportOption) => formatMemberForList(reportOption)); - const personalDetailsWithoutSelected = inviteOptions.personalDetails.filter(({login}) => !selectedLogins.includes(login)); - const personalDetailsFormatted = personalDetailsWithoutSelected.map((personalDetail) => formatMemberForList(personalDetail)); + const recentReportsModified = inviteOptions.recentReports.map((item) => (selectedLogins.includes(item.login) ? {...item, isSelected: true} : item)); + const recentReportsFormatted = recentReportsModified.map((reportOption) => formatMemberForList(reportOption)); + const personalDetailsModified = inviteOptions.personalDetails.map((item) => (selectedLogins.includes(item.login) ? {...item, isSelected: true} : item)); + const personalDetailsFormatted = personalDetailsModified.map((personalDetail) => formatMemberForList(personalDetail)); const hasUnselectedUserToInvite = inviteOptions.userToInvite && !selectedLogins.includes(inviteOptions.userToInvite.login); sectionsArr.push({ From 284a6955d5aea756c8d75ebaa42edd9f7c78fbb1 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Tue, 12 Aug 2025 20:41:00 +0530 Subject: [PATCH 07/50] prettier --- src/components/Search/SearchMultipleSelectionPicker.tsx | 2 +- src/pages/RoomInvitePage.tsx | 4 +++- src/pages/workspace/WorkspaceInvitePage.tsx | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index fed4cfe1378c..6740879c30f1 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -33,7 +33,7 @@ function SearchMultipleSelectionPicker({items, initiallySelectedItems, pickerTit }, [initiallySelectedItems]); const {sections, noResultsFound} = useMemo(() => { - const itemsSection = items + const itemsSection = items .filter((item) => item?.name?.toLowerCase().includes(debouncedSearchTerm?.toLowerCase())) .sort((a, b) => sortOptionsWithEmptyValue(a.value.toString(), b.value.toString(), localeCompare)) .map((item) => ({ diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 5e49838557d4..4d8353d50998 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -123,7 +123,9 @@ function RoomInvitePage({ } const selectedLogins = selectedOptions.map(({login}) => login); - const personalDetailsModified = Object.values(personalDetails).map((item) => selectedLogins.some((selectedLogin)=> item.login === selectedLogin) ? {...item, isSelected : true} : item); + const personalDetailsModified = Object.values(personalDetails).map((item) => + selectedLogins.some((selectedLogin) => item.login === selectedLogin) ? {...item, isSelected: true} : item, + ); const personalDetailsFormatted = personalDetailsModified.map((personalDetail) => formatMemberForList(personalDetail)); const hasUnselectedUserToInvite = userToInvite && !selectedLogins.includes(userToInvite.login); diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index e0abbc7a1961..e898d8ab233d 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -171,7 +171,9 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { } const selectedLogins = selectedOptions.map(({login}) => login); - const personalDetailsModified = Object.values(personalDetails).map((item) => selectedLogins.some((selectedLogin)=> item.login === selectedLogin) ? {...item, isSelected : true} : item); + const personalDetailsModified = Object.values(personalDetails).map((item) => + selectedLogins.some((selectedLogin) => item.login === selectedLogin) ? {...item, isSelected: true} : item, + ); const personalDetailsFormatted = personalDetailsModified.map((item) => formatMemberForList(item)); sectionsArr.push({ @@ -320,4 +322,3 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { WorkspaceInvitePage.displayName = 'WorkspaceInvitePage'; export default withNavigationTransitionEnd(withPolicyAndFullscreenLoading(WorkspaceInvitePage)); - From 0ee3e169ec03f2a8fe6da6beb1d9b7d195d191ee Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Tue, 12 Aug 2025 21:11:06 +0530 Subject: [PATCH 08/50] unify selected and non-selected options --- src/pages/NewChatPage.tsx | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 64e1d5a70955..eb81de3e0ec2 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -29,8 +29,6 @@ import Navigation from '@libs/Navigation/Navigation'; import type {Option, Section} from '@libs/OptionsListUtils'; import { filterAndOrderOptions, - filterSelectedOptions, - formatSectionsFromSearchTerm, getFirstKeyForList, getHeaderMessage, getPersonalDetailSearchTerms, @@ -76,21 +74,26 @@ function useOptions() { { betas: betas ?? [], includeSelfDM: true, + includeSelectedOptions: true, }, ); return filteredOptions; }, [betas, listOptions.personalDetails, listOptions.reports, contacts]); - const unselectedOptions = useMemo(() => filterSelectedOptions(defaultOptions, new Set(selectedOptions.map(({accountID}) => accountID))), [defaultOptions, selectedOptions]); + const defaultOptionsModified = { + ...defaultOptions, + recentReports: defaultOptions.recentReports.map((item) => selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true}:item), + personalDetails: defaultOptions.personalDetails.map((item) => selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true}:item), + } const options = useMemo(() => { - const filteredOptions = filterAndOrderOptions(unselectedOptions, debouncedSearchTerm, { + const filteredOptions = filterAndOrderOptions(defaultOptionsModified, debouncedSearchTerm, { selectedOptions, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, }); return filteredOptions; - }, [debouncedSearchTerm, unselectedOptions, selectedOptions]); + }, [debouncedSearchTerm, selectedOptions]); const cleanSearchTerm = useMemo(() => debouncedSearchTerm.trim().toLowerCase(), [debouncedSearchTerm]); const headerMessage = useMemo(() => { return getHeaderMessage( @@ -184,22 +187,6 @@ function NewChatPage(_: unknown, ref: React.Ref) { const sectionsList: Section[] = []; let firstKey = ''; - const formatResults = formatSectionsFromSearchTerm( - debouncedSearchTerm, - selectedOptions as OptionData[], - recentReports, - personalDetails, - undefined, - undefined, - undefined, - reportAttributesDerived, - ); - sectionsList.push(formatResults.section); - - if (!firstKey) { - firstKey = getFirstKeyForList(formatResults.section.data); - } - sectionsList.push({ title: translate('common.recents'), data: selectedOptions.length ? recentReports.filter((option) => !option.isSelfDM) : recentReports, @@ -394,7 +381,7 @@ function NewChatPage(_: unknown, ref: React.Ref) { headerMessage={headerMessage} onSelectRow={selectOption} shouldSingleExecuteRowSelect - onConfirm={(e, option) => (selectedOptions.length > 0 ? createGroup() : selectOption(option))} + onConfirm={(_e, option) => (selectedOptions.length > 0 ? createGroup() : selectOption(option))} rightHandSideComponent={itemRightSideComponent} footerContent={footerContent} showLoadingPlaceholder={!areOptionsInitialized} From c69c85658e1412159c15e1e0fa138bdb2c005d0c Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Tue, 12 Aug 2025 21:24:23 +0530 Subject: [PATCH 09/50] unify selected and non-selected options --- .../BaseOnboardingWorkspaceInvite.tsx | 26 ++----------------- 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx b/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx index ec8e3df2eaeb..872f63c8b220 100644 --- a/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx +++ b/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx @@ -157,31 +157,9 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo return []; } - // Filter all options that is a part of the search term or in the personal details - let filterSelectedOptions = selectedOptions; - if (debouncedSearchTerm !== '') { - filterSelectedOptions = selectedOptions.filter((option) => { - const accountID = option.accountID; - const isOptionInPersonalDetails = Object.values(personalDetails).some((personalDetail) => personalDetail.accountID === accountID); - - const searchValue = getSearchValueForPhoneOrEmail(debouncedSearchTerm); - - const isPartOfSearchTerm = !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); - return isPartOfSearchTerm || isOptionInPersonalDetails; - }); - } - - sectionsArr.push({ - title: undefined, - data: filterSelectedOptions, - shouldShow: true, - }); - - // Filtering out selected users from the search results const selectedLoginsSet = new Set(selectedOptions.map(({login}) => login)); - const personalDetailsFormatted = Object.values(personalDetails) - .filter(({login}) => !selectedLoginsSet.has(login ?? '')) - .map(formatMemberForList); + const personalDetailsModified = Object.values(personalDetails).map((item) => (selectedLoginsSet.has(item.login ?? '') ? {...item, isSelected: true} : item)); + const personalDetailsFormatted = personalDetailsModified.map((item) => formatMemberForList(item)); sectionsArr.push({ title: translate('common.contacts'), From cd6e0d795fa9a674c90e954e15f4aa1f8d913d0b Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Tue, 12 Aug 2025 21:57:59 +0530 Subject: [PATCH 10/50] unify selected and non-selected options --- .../SearchFiltersParticipantsSelector.tsx | 64 ++++--------------- 1 file changed, 13 insertions(+), 51 deletions(-) diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index d876c4b7b5c6..61830187145e 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -8,10 +8,9 @@ import useOnyx from '@hooks/useOnyx'; import useScreenWrapperTransitionStatus from '@hooks/useScreenWrapperTransitionStatus'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import memoize from '@libs/memoize'; -import {filterAndOrderOptions, filterSelectedOptions, formatSectionsFromSearchTerm, getValidOptions} from '@libs/OptionsListUtils'; +import {filterAndOrderOptions, getValidOptions} from '@libs/OptionsListUtils'; import type {Option, Section} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; -import {getDisplayNameForParticipant} from '@libs/ReportUtils'; import Navigation from '@navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -64,31 +63,31 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: { excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, includeCurrentUser: true, + includeSelectedOptions: true, }, ); }, [areOptionsInitialized, options.personalDetails, options.reports]); - const unselectedOptions = useMemo(() => { - return filterSelectedOptions(defaultOptions, new Set(selectedOptions.map((option) => option.accountID))); - }, [defaultOptions, selectedOptions]); + const defaultOptionsModified = { + ...defaultOptions, + recentReports: defaultOptions.recentReports.map((item) => + selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true} : item, + ), + personalDetails: defaultOptions.personalDetails.map((item) => + selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true} : item, + ), + }; const chatOptions = useMemo(() => { - const filteredOptions = filterAndOrderOptions(unselectedOptions, cleanSearchTerm, { + const filteredOptions = filterAndOrderOptions(defaultOptionsModified, cleanSearchTerm, { selectedOptions, excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, canInviteUser: false, }); - const {currentUserOption} = unselectedOptions; - - // Ensure current user is not in personalDetails when they should be excluded - if (currentUserOption) { - filteredOptions.personalDetails = filteredOptions.personalDetails.filter((detail) => detail.accountID !== currentUserOption.accountID); - } - return filteredOptions; - }, [unselectedOptions, cleanSearchTerm, selectedOptions]); + }, [defaultOptionsModified, cleanSearchTerm, selectedOptions]); const {sections, headerMessage} = useMemo(() => { const newSections: Section[] = []; @@ -96,43 +95,6 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: return {sections: [], headerMessage: undefined}; } - const formattedResults = formatSectionsFromSearchTerm( - cleanSearchTerm, - selectedOptions, - chatOptions.recentReports, - chatOptions.personalDetails, - personalDetails, - true, - undefined, - reportAttributesDerived, - ); - - const selectedCurrentUser = formattedResults.section.data.find((option) => option.accountID === chatOptions.currentUserOption?.accountID); - - // If the current user is already selected, remove them from the recent reports and personal details - if (selectedCurrentUser) { - chatOptions.recentReports = chatOptions.recentReports.filter((report) => report.accountID !== selectedCurrentUser.accountID); - chatOptions.personalDetails = chatOptions.personalDetails.filter((detail) => detail.accountID !== selectedCurrentUser.accountID); - } - - // If the current user is not selected, add them to the top of the list - if (!selectedCurrentUser && chatOptions.currentUserOption) { - const formattedName = getDisplayNameForParticipant({ - accountID: chatOptions.currentUserOption.accountID, - shouldAddCurrentUserPostfix: true, - personalDetailsData: personalDetails, - }); - chatOptions.currentUserOption.text = formattedName; - - newSections.push({ - title: '', - data: [chatOptions.currentUserOption], - shouldShow: true, - }); - } - - newSections.push(formattedResults.section); - newSections.push({ title: '', data: chatOptions.recentReports, From c85ee14a55f75e96fb1e289f82332b5e71fbcb6a Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Tue, 12 Aug 2025 22:10:39 +0530 Subject: [PATCH 11/50] prettier --- src/pages/NewChatPage.tsx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index eb81de3e0ec2..23e11aa8a57e 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -27,14 +27,7 @@ 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, - getFirstKeyForList, - getHeaderMessage, - getPersonalDetailSearchTerms, - getUserToInviteOption, - getValidOptions, -} from '@libs/OptionsListUtils'; +import {filterAndOrderOptions, getFirstKeyForList, getHeaderMessage, getPersonalDetailSearchTerms, getUserToInviteOption, getValidOptions} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -82,9 +75,13 @@ function useOptions() { const defaultOptionsModified = { ...defaultOptions, - recentReports: defaultOptions.recentReports.map((item) => selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true}:item), - personalDetails: defaultOptions.personalDetails.map((item) => selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true}:item), - } + recentReports: defaultOptions.recentReports.map((item) => + selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true} : item, + ), + personalDetails: defaultOptions.personalDetails.map((item) => + selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true} : item, + ), + }; const options = useMemo(() => { const filteredOptions = filterAndOrderOptions(defaultOptionsModified, debouncedSearchTerm, { From e2394b65509712d4840caee133bdcc10745e275c Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Tue, 12 Aug 2025 22:13:32 +0530 Subject: [PATCH 12/50] remove scroll to top test --- tests/unit/BaseSelectionListTest.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/unit/BaseSelectionListTest.tsx b/tests/unit/BaseSelectionListTest.tsx index 183757456808..1dbc0e8cd1b8 100644 --- a/tests/unit/BaseSelectionListTest.tsx +++ b/tests/unit/BaseSelectionListTest.tsx @@ -103,18 +103,6 @@ describe('BaseSelectionList', () => { expect(screen.getByTestId(`${CONST.BASE_LIST_ITEM_TEST_ID}2`)).toBeSelected(); }); - it('should scroll to top when selecting a multi option list', () => { - const spy = jest.spyOn(SectionList.prototype, 'scrollToLocation'); - render( - , - ); - fireEvent.press(screen.getByTestId(`${CONST.BASE_LIST_ITEM_TEST_ID}0`)); - expect(spy).toHaveBeenCalledWith(expect.objectContaining({itemIndex: 0})); - }); - it('should show only elements from first page and Show More button when items exceed page limit', () => { render( Date: Tue, 12 Aug 2025 22:17:50 +0530 Subject: [PATCH 13/50] remove scroll to top test --- tests/ui/NewChatPageTest.tsx | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/ui/NewChatPageTest.tsx b/tests/ui/NewChatPageTest.tsx index 15596a2611d2..5d99f71469cc 100644 --- a/tests/ui/NewChatPageTest.tsx +++ b/tests/ui/NewChatPageTest.tsx @@ -67,22 +67,6 @@ describe('NewChatPage', () => { await waitForBatchedUpdates(); }); - it('should scroll to top when adding a user to the group selection', async () => { - await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); - render(, {wrapper}); - await waitForBatchedUpdatesWithAct(); - act(() => { - (NativeNavigation as NativeNavigationMock).triggerTransitionEnd(); - }); - const spy = jest.spyOn(SectionList.prototype, 'scrollToLocation'); - - const addButton = await waitFor(() => screen.getAllByText(translateLocal('newChatPage.addToGroup')).at(0)); - if (addButton) { - fireEvent.press(addButton); - expect(spy).toHaveBeenCalledWith(expect.objectContaining({itemIndex: 0})); - } - }); - describe('should not display "Add to group" button on expensify emails', () => { const excludedGroupEmails = CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE && value !== CONST.EMAIL.NOTIFICATIONS).map((email) => [email]); From 76fb58e2bfd245531ac5ff1d6fe7191e36c1c9f5 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Tue, 12 Aug 2025 22:40:17 +0530 Subject: [PATCH 14/50] lint --- .../SearchFiltersParticipantsSelector.tsx | 22 ++++++++++--------- .../SelectionList/BaseSelectionList.tsx | 3 --- src/hooks/useWorkspaceList.ts | 2 +- src/pages/InviteReportParticipantsPage.tsx | 6 ++--- src/pages/NewChatPage.tsx | 8 +++---- .../BaseOnboardingWorkspaceInvite.tsx | 4 ++-- src/pages/RoomInvitePage.tsx | 3 +-- src/pages/workspace/WorkspaceInvitePage.tsx | 10 ++++----- tests/unit/BaseSelectionListTest.tsx | 1 - 9 files changed, 26 insertions(+), 33 deletions(-) diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index 61830187145e..5820b33f9fcf 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -68,15 +68,17 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: ); }, [areOptionsInitialized, options.personalDetails, options.reports]); - const defaultOptionsModified = { - ...defaultOptions, - recentReports: defaultOptions.recentReports.map((item) => - selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true} : item, - ), - personalDetails: defaultOptions.personalDetails.map((item) => - selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true} : item, - ), - }; + const defaultOptionsModified = useMemo(() => { + return { + ...defaultOptions, + recentReports: defaultOptions.recentReports.map((item) => + selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true} : item, + ), + personalDetails: defaultOptions.personalDetails.map((item) => + selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true} : item, + ), + }; + }, [defaultOptions]); const chatOptions = useMemo(() => { const filteredOptions = filterAndOrderOptions(defaultOptionsModified, cleanSearchTerm, { @@ -114,7 +116,7 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: sections: newSections, headerMessage: message, }; - }, [areOptionsInitialized, cleanSearchTerm, selectedOptions, chatOptions, personalDetails, reportAttributesDerived, translate]); + }, [areOptionsInitialized, chatOptions, translate]); const resetChanges = useCallback(() => { setSelectedOptions([]); diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 7926a418ac6d..ddf2a7839e6b 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -502,10 +502,7 @@ function BaseSelectionList( onSelectRow, shouldShowTextInput, shouldPreventDefaultFocusOnSelectRow, - sections.length, - isItemSelected, isSmallScreenWidth, - scrollToIndex, clearInputAfterSelect, onCheckboxPress, setFocusedIndex, diff --git a/src/hooks/useWorkspaceList.ts b/src/hooks/useWorkspaceList.ts index 29adc1a00fe2..6575fc224d10 100644 --- a/src/hooks/useWorkspaceList.ts +++ b/src/hooks/useWorkspaceList.ts @@ -62,7 +62,7 @@ function useWorkspaceList({policies, currentUserLogin, selectedPolicyIDs, search const filteredAndSortedUserWorkspaces = useMemo( () => tokenizedSearch(usersWorkspaces, searchTerm, (policy) => [policy.text]).sort((policy1, policy2) => localeCompare(policy1.text, policy2.text)), - [searchTerm, usersWorkspaces, selectedPolicyIDs], + [searchTerm, usersWorkspaces, selectedPolicyIDs, localeCompare], ); const sections = useMemo(() => { diff --git a/src/pages/InviteReportParticipantsPage.tsx b/src/pages/InviteReportParticipantsPage.tsx index 5c1b9f485a3c..d747a1c30a2b 100644 --- a/src/pages/InviteReportParticipantsPage.tsx +++ b/src/pages/InviteReportParticipantsPage.tsx @@ -27,7 +27,6 @@ import { getEmptyOptions, getHeaderMessage, getMemberInviteOptions, - getSearchValueForPhoneOrEmail, isPersonalDetailsReady, } from '@libs/OptionsListUtils'; import type {MemberForList} from '@libs/OptionsListUtils'; @@ -35,7 +34,6 @@ import {getLoginsByAccountIDs} from '@libs/PersonalDetailsUtils'; import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from '@libs/PhoneNumber'; import {getGroupChatName, getParticipantsAccountIDsForDisplay} from '@libs/ReportUtils'; import type {OptionData} from '@libs/ReportUtils'; -import tokenizedSearch from '@libs/tokenizedSearch'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -105,7 +103,7 @@ function InviteReportParticipantsPage({betas, report, didScreenTransitionEnd}: I setSelectedOptions(newSelectedOptions); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [personalDetails, betas, debouncedSearchTerm, excludedUsers, options]); + }, [personalDetails, betas, excludedUsers, options]); const sections = useMemo(() => { const sectionsArr: Sections = []; @@ -139,7 +137,7 @@ function InviteReportParticipantsPage({betas, report, didScreenTransitionEnd}: I } return sectionsArr; - }, [areOptionsInitialized, selectedOptions, debouncedSearchTerm, inviteOptions.recentReports, inviteOptions.personalDetails, inviteOptions.userToInvite, translate]); + }, [areOptionsInitialized, selectedOptions, inviteOptions.recentReports, inviteOptions.personalDetails, inviteOptions.userToInvite, translate]); const toggleOption = useCallback( (option: MemberForList) => { diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 23e11aa8a57e..6ddbc3e708d9 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -90,7 +90,7 @@ function useOptions() { }); return filteredOptions; - }, [debouncedSearchTerm, selectedOptions]); + }, [defaultOptionsModified, debouncedSearchTerm, selectedOptions]); const cleanSearchTerm = useMemo(() => debouncedSearchTerm.trim().toLowerCase(), [debouncedSearchTerm]); const headerMessage = useMemo(() => { return getHeaderMessage( @@ -170,15 +170,13 @@ function NewChatPage(_: unknown, ref: React.Ref) { const personalData = useCurrentUserPersonalDetails(); const {top} = useSafeAreaInsets(); const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true}); - const [reportAttributesDerived] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true, selector: (val) => val?.reports}); const selectionListRef = useRef(null); useImperativeHandle(ref, () => ({ focus: selectionListRef.current?.focusTextInput, })); - const {headerMessage, searchTerm, debouncedSearchTerm, setSearchTerm, selectedOptions, setSelectedOptions, recentReports, personalDetails, userToInvite, areOptionsInitialized} = - useOptions(); + const {headerMessage, searchTerm, setSearchTerm, selectedOptions, setSelectedOptions, recentReports, personalDetails, userToInvite, areOptionsInitialized} = useOptions(); const [sections, firstKeyForList] = useMemo(() => { const sectionsList: Section[] = []; @@ -214,7 +212,7 @@ function NewChatPage(_: unknown, ref: React.Ref) { } return [sectionsList, firstKey]; - }, [debouncedSearchTerm, selectedOptions, recentReports, personalDetails, reportAttributesDerived, translate, userToInvite]); + }, [selectedOptions, recentReports, personalDetails, translate, userToInvite]); /** * Removes a selected option from list if already selected. If not already selected add this option to the list. diff --git a/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx b/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx index 872f63c8b220..77aa89641606 100644 --- a/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx +++ b/src/pages/OnboardingWorkspaceInvite/BaseOnboardingWorkspaceInvite.tsx @@ -27,7 +27,7 @@ import HttpUtils from '@libs/HttpUtils'; import {appendCountryCode} from '@libs/LoginUtils'; import {navigateAfterOnboardingWithMicrotaskQueue} from '@libs/navigateAfterOnboarding'; import type {MemberForList} from '@libs/OptionsListUtils'; -import {filterAndOrderOptions, formatMemberForList, getHeaderMessage, getMemberInviteOptions, getSearchValueForPhoneOrEmail} from '@libs/OptionsListUtils'; +import {filterAndOrderOptions, formatMemberForList, getHeaderMessage, getMemberInviteOptions} from '@libs/OptionsListUtils'; import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from '@libs/PhoneNumber'; import {getIneligibleInvitees, getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils'; import type {OptionData} from '@libs/ReportUtils'; @@ -180,7 +180,7 @@ function BaseOnboardingWorkspaceInvite({shouldUseNativeStyles}: BaseOnboardingWo }); return sectionsArr; - }, [areOptionsInitialized, selectedOptions, debouncedSearchTerm, personalDetails, translate, usersToInvite]); + }, [areOptionsInitialized, selectedOptions, personalDetails, translate, usersToInvite]); const toggleOption = (option: MemberForList) => { const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option.login); diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 4d8353d50998..1c379160e23f 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -1,4 +1,3 @@ -import {Str} from 'expensify-common'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {SectionListData} from 'react-native'; import {View} from 'react-native'; @@ -142,7 +141,7 @@ function RoomInvitePage({ } return sectionsArr; - }, [inviteOptions, areOptionsInitialized, selectedOptions, debouncedSearchTerm, translate]); + }, [inviteOptions, areOptionsInitialized, selectedOptions, translate]); const toggleOption = useCallback( (option: MemberForList) => { diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index e898d8ab233d..d1446db16e68 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -23,7 +23,7 @@ import HttpUtils from '@libs/HttpUtils'; import {appendCountryCode} from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import {filterAndOrderOptions, formatMemberForList, getHeaderMessage, getMemberInviteOptions, getSearchValueForPhoneOrEmail} from '@libs/OptionsListUtils'; +import {filterAndOrderOptions, formatMemberForList, getHeaderMessage, getMemberInviteOptions} from '@libs/OptionsListUtils'; import type {MemberForList} from '@libs/OptionsListUtils'; import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from '@libs/PhoneNumber'; import {getIneligibleInvitees, getMemberAccountIDsForWorkspace, goBackFromInvalidPolicy} from '@libs/PolicyUtils'; @@ -54,10 +54,10 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { const [personalDetails, setPersonalDetails] = useState([]); const [usersToInvite, setUsersToInvite] = useState([]); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); + const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true}); const firstRenderRef = useRef(true); - const [betas] = useOnyx(ONYXKEYS.BETAS); - const [invitedEmailsToAccountIDsDraft] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`); + const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); + const [invitedEmailsToAccountIDsDraft] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`, {canBeMissing: true}); const openWorkspaceInvitePage = () => { const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList); @@ -195,7 +195,7 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { }); return sectionsArr; - }, [areOptionsInitialized, selectedOptions, debouncedSearchTerm, personalDetails, translate, usersToInvite]); + }, [areOptionsInitialized, selectedOptions, personalDetails, translate, usersToInvite]); const toggleOption = (option: MemberForList) => { clearErrors(route.params.policyID); diff --git a/tests/unit/BaseSelectionListTest.tsx b/tests/unit/BaseSelectionListTest.tsx index 1dbc0e8cd1b8..febf4d6fe8a9 100644 --- a/tests/unit/BaseSelectionListTest.tsx +++ b/tests/unit/BaseSelectionListTest.tsx @@ -1,7 +1,6 @@ import * as NativeNavigation from '@react-navigation/native'; import {fireEvent, render, screen} from '@testing-library/react-native'; import {useState} from 'react'; -import {SectionList} from 'react-native'; import BaseSelectionList from '@components/SelectionList/BaseSelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import type {ListItem, SelectionListProps} from '@components/SelectionList/types'; From 4adf1a683d5c63c897c17d7bc9086912bcc9010e Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Tue, 12 Aug 2025 22:42:06 +0530 Subject: [PATCH 15/50] prettier --- src/pages/InviteReportParticipantsPage.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/pages/InviteReportParticipantsPage.tsx b/src/pages/InviteReportParticipantsPage.tsx index d747a1c30a2b..a767a93e0ab9 100644 --- a/src/pages/InviteReportParticipantsPage.tsx +++ b/src/pages/InviteReportParticipantsPage.tsx @@ -21,14 +21,7 @@ import {appendCountryCode} from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ParticipantsNavigatorParamList} from '@libs/Navigation/types'; -import { - filterAndOrderOptions, - formatMemberForList, - getEmptyOptions, - getHeaderMessage, - getMemberInviteOptions, - isPersonalDetailsReady, -} from '@libs/OptionsListUtils'; +import {filterAndOrderOptions, formatMemberForList, getEmptyOptions, getHeaderMessage, getMemberInviteOptions, isPersonalDetailsReady} from '@libs/OptionsListUtils'; import type {MemberForList} from '@libs/OptionsListUtils'; import {getLoginsByAccountIDs} from '@libs/PersonalDetailsUtils'; import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from '@libs/PhoneNumber'; From 58b1e5ebe45f0024d1f296f352d798b9ea069caf Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Tue, 19 Aug 2025 20:25:18 +0530 Subject: [PATCH 16/50] stop separating selected and non selected options --- src/libs/OptionsListUtils.ts | 8 ++--- .../request/MoneyRequestAttendeeSelector.tsx | 29 +++++++++++++++++-- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 5c83d764202c..cc9123c674ae 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2164,8 +2164,8 @@ function getAttendeeOptions( }); } - const filteredRecentAttendees = recentAttendees - .filter((attendee) => !attendees.find(({email, displayName}) => (attendee.email ? email === attendee.email : displayName === attendee.displayName))) + // attendees list might lack some personal data such as accoundID and login at this point - fetch those here + const RecentAttendeesWithDetails = recentAttendees .map((attendee) => ({ ...attendee, login: attendee.email ?? attendee.displayName, @@ -2182,11 +2182,11 @@ function getAttendeeOptions( includeOwnedWorkspaceChats, includeRecentReports: false, includeP2P, - includeSelectedOptions: false, + includeSelectedOptions: true, includeSelfDM: false, includeInvoiceRooms, action, - recentAttendees: filteredRecentAttendees, + recentAttendees: RecentAttendeesWithDetails, }, ); } diff --git a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx index 4fb7a21cfff1..a61faad6f1f3 100644 --- a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx +++ b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx @@ -98,6 +98,19 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde return optionList; }, [areOptionsInitialized, didScreenTransitionEnd, options.reports, options.personalDetails, betas, attendees, recentAttendees, iouType, action, isPaidGroupPolicy, searchTerm]); + // Selectd members list is maintained saparately as 'attendees', so update selection info here + const defaultOptionsModified = useMemo(() => { + return { + ...defaultOptions, + recentReports: defaultOptions.recentReports.map((item) => + attendees.some((attendee) => attendee?.email === item.login || attendee?.accountID === item.accountID) ? {...item, isSelected: true} : item, + ), + personalDetails: defaultOptions.personalDetails.map((item) => + attendees.some((attendee) => attendee?.email === item.login || attendee?.accountID === item.accountID) ? {...item, isSelected: true} : item, + ), + }; + }, [defaultOptions]); + const chatOptions = useMemo(() => { if (!areOptionsInitialized) { return { @@ -108,7 +121,7 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde headerMessage: '', }; } - const newOptions = filterAndOrderOptions(defaultOptions, cleanSearchTerm, { + const newOptions = filterAndOrderOptions(defaultOptionsModified, cleanSearchTerm, { excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, preferPolicyExpenseChat: isPaidGroupPolicy, shouldAcceptName: true, @@ -120,9 +133,21 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde ...getPersonalDetailByEmail(attendee.email), })), }); + return newOptions; }, [areOptionsInitialized, defaultOptions, cleanSearchTerm, isPaidGroupPolicy, attendees]); + // attendees who are not on expensify + const filteredAttendees = attendees + .filter((attendee) => attendee.accountID) + .map((attendee) => ({ + ...attendee, + reportID: CONST.DEFAULT_NUMBER_ID.toString(), + selected: true, + login: attendee.email, + ...getPersonalDetailByEmail(attendee.email), + })); + /** * Returns the sections needed for the OptionsSelector */ @@ -138,7 +163,7 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde const formatResults = formatSectionsFromSearchTerm( cleanSearchTerm, - attendees.map((attendee) => ({ + filteredAttendees.map((attendee) => ({ ...attendee, reportID: CONST.DEFAULT_NUMBER_ID.toString(), selected: true, From f07cd203ce48307fff35325e484b6fc3fd6fc7eb Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 20 Aug 2025 17:42:17 +0530 Subject: [PATCH 17/50] stop separating selected and non selected options --- .../Search/SearchFiltersChatsSelector.tsx | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx index 10521d992e9e..3f39d8f71313 100644 --- a/src/components/Search/SearchFiltersChatsSelector.tsx +++ b/src/components/Search/SearchFiltersChatsSelector.tsx @@ -9,7 +9,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useScreenWrapperTransitionStatus from '@hooks/useScreenWrapperTransitionStatus'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; -import {createOptionFromReport, filterAndOrderOptions, formatSectionsFromSearchTerm, getAlternateText, getSearchOptions} from '@libs/OptionsListUtils'; +import {createOptionFromReport, filterAndOrderOptions, getAlternateText, getSearchOptions} from '@libs/OptionsListUtils'; import type {Option, Section} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; import Navigation from '@navigation/Navigation'; @@ -67,12 +67,19 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen return getSearchOptions(options, undefined, false); }, [areOptionsInitialized, isScreenTransitionEnd, options]); + const defaultOptionsModified = useMemo(() => { + return { + ...defaultOptions, + recentReports: defaultOptions.recentReports.map((item) => (selectedReportIDs.includes(item.reportID) ? {...item, isSelected: true} : item)), + }; + }, [defaultOptions, selectedReportIDs]); + const chatOptions = useMemo(() => { - return filterAndOrderOptions(defaultOptions, cleanSearchTerm, { + return filterAndOrderOptions(defaultOptionsModified, cleanSearchTerm, { selectedOptions, excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, }); - }, [defaultOptions, cleanSearchTerm, selectedOptions]); + }, [defaultOptions, cleanSearchTerm, selectedOptions, defaultOptionsModified]); const {sections, headerMessage} = useMemo(() => { const newSections: Section[] = []; @@ -80,30 +87,13 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen return {sections: [], headerMessage: undefined}; } - const formattedResults = formatSectionsFromSearchTerm( - cleanSearchTerm, - selectedOptions, - chatOptions.recentReports, - chatOptions.personalDetails, - personalDetails, - false, - undefined, - reportAttributesDerived, - ); - - newSections.push(formattedResults.section); - - const visibleReportsWhenSearchTermNonEmpty = chatOptions.recentReports.map((report) => (selectedReportIDs.includes(report.reportID) ? getSelectedOptionData(report) : report)); - const visibleReportsWhenSearchTermEmpty = chatOptions.recentReports.filter((report) => !selectedReportIDs.includes(report.reportID)); - const reportsFiltered = cleanSearchTerm === '' ? visibleReportsWhenSearchTermEmpty : visibleReportsWhenSearchTermNonEmpty; - newSections.push({ title: undefined, - data: reportsFiltered, + data: chatOptions.recentReports, shouldShow: chatOptions.recentReports.length > 0, }); - const areResultsFound = didScreenTransitionEnd && formattedResults.section.data.length === 0 && reportsFiltered.length === 0; + const areResultsFound = didScreenTransitionEnd && chatOptions.recentReports.length === 0; const message = areResultsFound ? translate('common.noResultsFound') : undefined; return { From 3a409350b1f45c11d983ab565a0de3f8b61a668a Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 20 Aug 2025 17:42:24 +0530 Subject: [PATCH 18/50] stop separating selected and non selected options --- src/components/Search/SearchFiltersParticipantsSelector.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index 5820b33f9fcf..cc53b092b0e5 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -45,7 +45,6 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: shouldInitialize: didScreenTransitionEnd, }); const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {canBeMissing: false, initWithStoredValues: false}); - const [reportAttributesDerived] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true, selector: (val) => val?.reports}); const [selectedOptions, setSelectedOptions] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const cleanSearchTerm = useMemo(() => searchTerm.trim().toLowerCase(), [searchTerm]); @@ -78,7 +77,7 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true} : item, ), }; - }, [defaultOptions]); + }, [defaultOptions, selectedOptions]); const chatOptions = useMemo(() => { const filteredOptions = filterAndOrderOptions(defaultOptionsModified, cleanSearchTerm, { From a71c0acc2cc9b4637e53efa712c78606b046ce9a Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 20 Aug 2025 17:43:10 +0530 Subject: [PATCH 19/50] stop separating selected and non selected options --- src/hooks/useWorkspaceList.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useWorkspaceList.ts b/src/hooks/useWorkspaceList.ts index 6575fc224d10..c7289c4f4d18 100644 --- a/src/hooks/useWorkspaceList.ts +++ b/src/hooks/useWorkspaceList.ts @@ -62,7 +62,7 @@ function useWorkspaceList({policies, currentUserLogin, selectedPolicyIDs, search const filteredAndSortedUserWorkspaces = useMemo( () => tokenizedSearch(usersWorkspaces, searchTerm, (policy) => [policy.text]).sort((policy1, policy2) => localeCompare(policy1.text, policy2.text)), - [searchTerm, usersWorkspaces, selectedPolicyIDs, localeCompare], + [searchTerm, usersWorkspaces, localeCompare], ); const sections = useMemo(() => { From b8eeb6c9f7c4c93eef6a38c1449f9c02dac71af9 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 20 Aug 2025 17:43:40 +0530 Subject: [PATCH 20/50] stop separating selected and non selected options --- src/libs/CardFeedUtils.ts | 31 +++---------- .../SearchFiltersCardPage.tsx | 43 +++++++------------ 2 files changed, 23 insertions(+), 51 deletions(-) diff --git a/src/libs/CardFeedUtils.ts b/src/libs/CardFeedUtils.ts index d1190ea5fdfc..32d81e54b7e7 100644 --- a/src/libs/CardFeedUtils.ts +++ b/src/libs/CardFeedUtils.ts @@ -22,7 +22,7 @@ import type {OptionData} from './ReportUtils'; type CardFilterItem = Partial & AdditionalCardProps & {isCardFeed?: boolean; correspondingCards?: string[]; cardFeedKey: string; plaidUrl?: string}; type DomainFeedData = {bank: string; domainName: string; correspondingCardIDs: string[]; fundID?: string}; -type ItemsGroupedBySelection = {selected: CardFilterItem[]; unselected: CardFilterItem[]}; +type ItemsGroupedBySelection = CardFilterItem[]; type CardFeedNamesWithType = Record; type CardFeedData = {cardName: string; bank: string; label?: string; type: 'domain' | 'workspace'}; type GetCardFeedData = { @@ -128,16 +128,8 @@ function buildCardsData( }); const allCardItems = [...userAssignedCards, ...allWorkspaceCards]; - const selectedCardItems: CardFilterItem[] = []; - const unselectedCardItems: CardFilterItem[] = []; - allCardItems.forEach((card) => { - if (card.isSelected) { - selectedCardItems.push(card); - } else { - unselectedCardItems.push(card); - } - }); - return {selected: selectedCardItems, unselected: unselectedCardItems}; + + return allCardItems; } /** @@ -316,8 +308,7 @@ function buildCardFeedsData( translate: LocaleContextProps['translate'], illustrations: IllustrationsType, ): ItemsGroupedBySelection { - const selectedFeeds: CardFilterItem[] = []; - const unselectedFeeds: CardFilterItem[] = []; + const cardFeed: CardFilterItem[] = []; const repeatingBanks = getRepeatingBanks(Object.keys(workspaceCardFeeds), domainFeedsData); Object.values(domainFeedsData).forEach((domainFeed) => { @@ -335,11 +326,7 @@ function buildCardFeedsData( selectedCards, illustrations, }); - if (feedItem.isSelected) { - selectedFeeds.push(feedItem); - } else { - unselectedFeeds.push(feedItem); - } + cardFeed.push(feedItem); }); filterOutDomainCards(workspaceCardFeeds).forEach(([workspaceFeedKey, workspaceFeed]) => { @@ -363,14 +350,10 @@ function buildCardFeedsData( selectedCards, illustrations, }); - if (feedItem.isSelected) { - selectedFeeds.push(feedItem); - } else { - unselectedFeeds.push(feedItem); - } + cardFeed.push(feedItem); }); - return {selected: selectedFeeds, unselected: unselectedFeeds}; + return cardFeed; } function getSelectedCardsFromFeeds(cards: CardList | undefined, workspaceCardFeeds?: Record, selectedFeeds?: string[]): string[] { diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx index 167cc3fa74d8..f1f22b5bebeb 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx @@ -60,8 +60,7 @@ function SearchFiltersCardPage() { ); const shouldShowSearchInput = - cardFeedsSectionData.selected.length + cardFeedsSectionData.unselected.length + individualCardsSectionData.selected.length + individualCardsSectionData.unselected.length > - CONST.COMPANY_CARDS.CARD_LIST_THRESHOLD; + cardFeedsSectionData.length + cardFeedsSectionData.length + individualCardsSectionData.length + individualCardsSectionData.length > CONST.COMPANY_CARDS.CARD_LIST_THRESHOLD; const searchFunction = useCallback( (item: CardFilterItem) => @@ -78,43 +77,33 @@ function SearchFiltersCardPage() { } const newSections = []; - const selectedItems = [...cardFeedsSectionData.selected, ...individualCardsSectionData.selected, ...closedCardsSectionData.selected]; + // const selectedItems = [...cardFeedsSectionData.selected, ...individualCardsSectionData.selected, ...closedCardsSectionData.selected]; - newSections.push({ - title: undefined, - data: selectedItems.filter(searchFunction), - shouldShow: selectedItems.length > 0, - }); + // newSections.push({ + // title: undefined, + // data: selectedItems.filter(searchFunction), + // shouldShow: selectedItems.length > 0, + // }); newSections.push({ title: translate('search.filters.card.cardFeeds'), - data: cardFeedsSectionData.unselected.filter(searchFunction), - shouldShow: cardFeedsSectionData.unselected.length > 0, + data: cardFeedsSectionData.filter(searchFunction), + shouldShow: cardFeedsSectionData.length > 0, }); newSections.push({ title: translate('search.filters.card.individualCards'), - data: individualCardsSectionData.unselected.filter(searchFunction), - shouldShow: individualCardsSectionData.unselected.length > 0, + data: individualCardsSectionData.filter(searchFunction), + shouldShow: individualCardsSectionData.length > 0, }); newSections.push({ title: translate('search.filters.card.closedCards'), - data: closedCardsSectionData.unselected.filter(searchFunction), - shouldShow: closedCardsSectionData.unselected.length > 0, + data: closedCardsSectionData.filter(searchFunction), + shouldShow: closedCardsSectionData.length > 0, }); return newSections; - }, [ - searchAdvancedFiltersForm, - cardFeedsSectionData.selected, - cardFeedsSectionData.unselected, - individualCardsSectionData.selected, - individualCardsSectionData.unselected, - closedCardsSectionData.selected, - closedCardsSectionData.unselected, - searchFunction, - translate, - ]); + }, [searchAdvancedFiltersForm, cardFeedsSectionData, individualCardsSectionData, closedCardsSectionData, searchFunction, translate]); const handleConfirmSelection = useCallback(() => { - const feeds = cardFeedsSectionData.selected.map((feed) => feed.cardFeedKey); + const feeds = cardFeedsSectionData.filter((feed) => feed.isSelected).map((feed) => feed.cardFeedKey); const cardsFromSelectedFeed = getSelectedCardsFromFeeds(userCardList, workspaceCardFeeds, feeds); const IDs = selectedCards.filter((card) => !cardsFromSelectedFeed.includes(card)); @@ -124,7 +113,7 @@ function SearchFiltersCardPage() { }); Navigation.goBack(ROUTES.SEARCH_ADVANCED_FILTERS); - }, [userCardList, selectedCards, cardFeedsSectionData.selected, workspaceCardFeeds]); + }, [userCardList, selectedCards, cardFeedsSectionData, workspaceCardFeeds]); const updateNewCards = useCallback( (item: CardFilterItem) => { From c0e302458ee9d106685bfaa42543cff537922cda Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 20 Aug 2025 17:44:20 +0530 Subject: [PATCH 21/50] lint --- src/pages/NewChatPage.tsx | 20 ++++++++++--------- .../request/MoneyRequestAttendeeSelector.tsx | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 6ddbc3e708d9..4709d7725c74 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -73,15 +73,17 @@ function useOptions() { return filteredOptions; }, [betas, listOptions.personalDetails, listOptions.reports, contacts]); - const defaultOptionsModified = { - ...defaultOptions, - recentReports: defaultOptions.recentReports.map((item) => - selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true} : item, - ), - personalDetails: defaultOptions.personalDetails.map((item) => - selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true} : item, - ), - }; + const defaultOptionsModified = useMemo(() => { + return { + ...defaultOptions, + recentReports: defaultOptions.recentReports.map((item) => + selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true} : item, + ), + personalDetails: defaultOptions.personalDetails.map((item) => + selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true} : item, + ), + }; + }, [defaultOptions, selectedOptions]); const options = useMemo(() => { const filteredOptions = filterAndOrderOptions(defaultOptionsModified, debouncedSearchTerm, { diff --git a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx index a61faad6f1f3..5f8c6629f0d7 100644 --- a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx +++ b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx @@ -109,7 +109,7 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde attendees.some((attendee) => attendee?.email === item.login || attendee?.accountID === item.accountID) ? {...item, isSelected: true} : item, ), }; - }, [defaultOptions]); + }, [defaultOptions, attendees]); const chatOptions = useMemo(() => { if (!areOptionsInitialized) { From ab1e6ae1e779576f3a194a4429fedb0a37cc406c Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 20 Aug 2025 18:46:56 +0530 Subject: [PATCH 22/50] lint --- src/components/Search/SearchFiltersChatsSelector.tsx | 7 +------ src/libs/OptionsListUtils.ts | 2 +- src/pages/iou/request/MoneyRequestAttendeeSelector.tsx | 5 +++-- tests/ui/NewChatPageTest.tsx | 2 -- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx index 3f39d8f71313..7732d3c49ee2 100644 --- a/src/components/Search/SearchFiltersChatsSelector.tsx +++ b/src/components/Search/SearchFiltersChatsSelector.tsx @@ -79,7 +79,7 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen selectedOptions, excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, }); - }, [defaultOptions, cleanSearchTerm, selectedOptions, defaultOptionsModified]); + }, [cleanSearchTerm, selectedOptions, defaultOptionsModified]); const {sections, headerMessage} = useMemo(() => { const newSections: Section[] = []; @@ -102,14 +102,9 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen }; }, [ areOptionsInitialized, - chatOptions.personalDetails, chatOptions.recentReports, - cleanSearchTerm, didScreenTransitionEnd, - personalDetails, reportAttributesDerived, - selectedOptions, - selectedReportIDs, translate, ]); diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index d00fcc7ad2a0..232df2dad90e 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2168,7 +2168,7 @@ function getAttendeeOptions( }); } - // attendees list might lack some personal data such as accoundID and login at this point - fetch those here + // attendees list might lack some personal data such as accountID and login at this point - fetch those here const RecentAttendeesWithDetails = recentAttendees .map((attendee) => ({ ...attendee, diff --git a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx index 5f8c6629f0d7..482e8688d6cd 100644 --- a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx +++ b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx @@ -98,7 +98,7 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde return optionList; }, [areOptionsInitialized, didScreenTransitionEnd, options.reports, options.personalDetails, betas, attendees, recentAttendees, iouType, action, isPaidGroupPolicy, searchTerm]); - // Selectd members list is maintained saparately as 'attendees', so update selection info here + // Selected members list is maintained separately as 'attendees', so update selection info here const defaultOptionsModified = useMemo(() => { return { ...defaultOptions, @@ -135,7 +135,7 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde }); return newOptions; - }, [areOptionsInitialized, defaultOptions, cleanSearchTerm, isPaidGroupPolicy, attendees]); + }, [areOptionsInitialized, defaultOptionsModified, cleanSearchTerm, isPaidGroupPolicy, attendees]); // attendees who are not on expensify const filteredAttendees = attendees @@ -216,6 +216,7 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde }, [ areOptionsInitialized, didScreenTransitionEnd, + filteredAttendees, chatOptions.recentReports, chatOptions.personalDetails, chatOptions.userToInvite, diff --git a/tests/ui/NewChatPageTest.tsx b/tests/ui/NewChatPageTest.tsx index 5d99f71469cc..5648eb40b1ed 100644 --- a/tests/ui/NewChatPageTest.tsx +++ b/tests/ui/NewChatPageTest.tsx @@ -1,7 +1,6 @@ import * as NativeNavigation from '@react-navigation/native'; import {act, fireEvent, render, screen, waitFor, within} from '@testing-library/react-native'; import React from 'react'; -import {SectionList} from 'react-native'; import Onyx from 'react-native-onyx'; import HTMLEngineProvider from '@components/HTMLEngineProvider'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; @@ -13,7 +12,6 @@ import NewChatPage from '@pages/NewChatPage'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {NativeNavigationMock} from '../../__mocks__/@react-navigation/native'; -import {fakePersonalDetails} from '../utils/LHNTestUtils'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; From df92e4eea011368f449752a8f80d0173cfef7521 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 20 Aug 2025 18:48:17 +0530 Subject: [PATCH 23/50] fix card feed test --- tests/unit/Search/buildCardFilterDataTest.ts | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/unit/Search/buildCardFilterDataTest.ts b/tests/unit/Search/buildCardFilterDataTest.ts index 7f5d3a2d3fae..7fb8d4ddb009 100644 --- a/tests/unit/Search/buildCardFilterDataTest.ts +++ b/tests/unit/Search/buildCardFilterDataTest.ts @@ -330,17 +330,17 @@ describe('buildIndividualCardsData', () => { illustrationsMock as IllustrationsType, ); - expect(result.unselected.length + result.selected.length).toEqual(13); + expect(result.length).toEqual(13); // Check if Expensify card was built correctly - const expensifyCard = result.selected.find((card) => card.keyForList === '21588678'); + const expensifyCard = result.find((card) => card.keyForList === '21588678'); expect(expensifyCard).toMatchObject({ lastFourPAN: '1138', isSelected: true, }); // Check if company card was built correctly - const companyCard = result.unselected.find((card) => card.keyForList === '21604933'); + const companyCard = result.find((card) => card.keyForList === '21604933'); expect(companyCard).toMatchObject({ lastFourPAN: '1601', isSelected: false, @@ -354,7 +354,7 @@ describe('buildIndividualCardsData', () => { [], illustrationsMock as IllustrationsType, ); - expect(result.unselected.length + result.selected.length).toEqual(0); + expect(result.length).toEqual(0); }); }); @@ -368,17 +368,17 @@ describe('buildCardsData closed cards', () => { illustrationsMock as IllustrationsType, true, ); - expect(result.unselected.length + result.selected.length).toEqual(4); + expect(result.length).toEqual(4); // Check if Expensify card was built correctly - const expensifyCard = result.selected.find((card) => card.keyForList === '21539012'); + const expensifyCard = result.find((card) => card.keyForList === '21539012'); expect(expensifyCard).toMatchObject({ lastFourPAN: '3211', isSelected: true, }); // Check if company card was built correctly - const companyCard = result.unselected.find((card) => card.keyForList === '21534525'); + const companyCard = result.find((card) => card.keyForList === '21534525'); expect(companyCard).toMatchObject({ lastFourPAN: '', isSelected: false, @@ -404,32 +404,32 @@ describe('buildCardFeedsData', () => { it('Build domain card feed properly', () => { // Check if external domain feed was built properly - expect(result.unselected.at(0)).toMatchObject({ + expect(result.at(0)).toMatchObject({ isCardFeed: true, correspondingCards: ['21589168', '21589182'], }); expect(translateMock).toHaveBeenCalledWith('search.filters.card.cardFeedName', {cardFeedBankName: undefined, cardFeedLabel: 'mockDomain.com'}); // Check if domain card feed was built properly - expect(result.unselected.at(1)).toMatchObject({ + expect(result.at(1)).toMatchObject({ isCardFeed: true, correspondingCards: ['21593492', '21604933', '21638320', '21638598'], }); expect(translateMock).toHaveBeenCalledWith('search.filters.card.cardFeedName', {cardFeedBankName: 'Visa', cardFeedLabel: undefined}); // Check if workspace card feed that comes from company cards was built properly. - expect(result.unselected.at(2)).toMatchObject({ + expect(result.at(2)).toMatchObject({ isCardFeed: true, correspondingCards: ['21588678', '21588684'], }); expect(translateMock).toHaveBeenCalledWith('search.filters.card.cardFeedName', {cardFeedBankName: undefined, cardFeedLabel: 'test1'}); // Check if workspace card feed that comes from expensify cards was built properly - expect(result.unselected.at(3)).toMatchObject({ + expect(result.at(3)).toMatchObject({ isCardFeed: true, correspondingCards: ['21589168', '21589182', '21589202', '21638322'], }); expect(translateMock).toHaveBeenCalledWith('search.filters.card.cardFeedName', {cardFeedBankName: undefined, cardFeedLabel: 'test2'}); // Check if domain card feed was built properly - expect(result.unselected.length).toEqual(4); + expect(result.length).toEqual(4); }); }); From 5acd83f558d9b853312a4cc28d5cda81b030045d Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 20 Aug 2025 18:53:36 +0530 Subject: [PATCH 24/50] prettier --- src/components/Search/SearchFiltersChatsSelector.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx index 7732d3c49ee2..ea441b4ca705 100644 --- a/src/components/Search/SearchFiltersChatsSelector.tsx +++ b/src/components/Search/SearchFiltersChatsSelector.tsx @@ -100,13 +100,7 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen sections: newSections, headerMessage: message, }; - }, [ - areOptionsInitialized, - chatOptions.recentReports, - didScreenTransitionEnd, - reportAttributesDerived, - translate, - ]); + }, [areOptionsInitialized, chatOptions.recentReports, didScreenTransitionEnd, reportAttributesDerived, translate]); useEffect(() => { searchInServer(debouncedSearchTerm.trim()); From 11fd58a9470f5b7e9cf8bb9fcc162d300397696b Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 20 Aug 2025 18:53:58 +0530 Subject: [PATCH 25/50] fix card feed test --- tests/unit/Search/buildCardFilterDataTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/Search/buildCardFilterDataTest.ts b/tests/unit/Search/buildCardFilterDataTest.ts index 7fb8d4ddb009..3abc09ef89b0 100644 --- a/tests/unit/Search/buildCardFilterDataTest.ts +++ b/tests/unit/Search/buildCardFilterDataTest.ts @@ -389,7 +389,7 @@ describe('buildCardsData closed cards', () => { describe('buildCardsData with empty argument objects', () => { it('Returns empty array when cardList and workspaceCardFeeds are empty', () => { const result = buildCardsData({}, {}, {}, [], illustrationsMock as IllustrationsType); - expect(result).toEqual({selected: [], unselected: []}); + expect(result).toEqual([]); }); }); From 5a90f957d9ed3661f65aee299d5ac9edea51694a Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 20 Aug 2025 21:00:18 +0530 Subject: [PATCH 26/50] fix card feed test --- tests/unit/Search/buildCardFilterDataTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/Search/buildCardFilterDataTest.ts b/tests/unit/Search/buildCardFilterDataTest.ts index 3abc09ef89b0..edd2203f9818 100644 --- a/tests/unit/Search/buildCardFilterDataTest.ts +++ b/tests/unit/Search/buildCardFilterDataTest.ts @@ -436,6 +436,6 @@ describe('buildCardFeedsData', () => { describe('buildIndividualCardsData with empty argument objects', () => { it('Return empty array when domainCardFeeds and workspaceCardFeeds are empty', () => { const result = buildCardFeedsData({}, {}, [], translateMock as LocaleContextProps['translate'], illustrationsMock as IllustrationsType); - expect(result).toEqual({selected: [], unselected: []}); + expect(result).toEqual([]); }); }); From 5d25a51161c34cef145443f5a39091f55b8f190a Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 27 Aug 2025 19:54:42 +0530 Subject: [PATCH 27/50] do not animate for pending scroll ref --- src/components/SelectionList/BaseSelectionList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index ddf2a7839e6b..be49c37f3a60 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -399,7 +399,7 @@ function BaseSelectionList( if (targetItem && indexToScroll < CONST.MAX_SELECTION_LIST_PAGE_LENGTH * currentPage) { pendingScrollIndexRef.current = null; - scrollToIndex(indexToScroll, true); + scrollToIndex(indexToScroll, false); } }, [currentPage, scrollToIndex, flattenedSections.allOptions]); From c0bf6c294c2d53eaf1eaed3b9d62aa5b6a52d188 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 27 Aug 2025 20:28:02 +0530 Subject: [PATCH 28/50] avoid clearing focus index for initial render --- src/components/SelectionList/BaseSelectionList.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index be49c37f3a60..97b9d6b19e7c 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -830,6 +830,9 @@ function BaseSelectionList( } } + // Avoid clearing focus on initial render + if (isInitialSectionListRender) return; + // Remove the focus if the search input is empty and prev search input not empty or selected options length is changed (and allOptions length remains the same) // else focus on the first non disabled item const newSelectedIndex = From 0b6de55aab79d0b773b4b07d1d06fe9ad1b988ee Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 27 Aug 2025 21:15:55 +0530 Subject: [PATCH 29/50] add initiallyFocusedOptionKey --- .../Search/SearchFiltersChatsSelector.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx index ea441b4ca705..e358ed686019 100644 --- a/src/components/Search/SearchFiltersChatsSelector.tsx +++ b/src/components/Search/SearchFiltersChatsSelector.tsx @@ -13,6 +13,7 @@ import {createOptionFromReport, filterAndOrderOptions, getAlternateText, getSear import type {Option, Section} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; import Navigation from '@navigation/Navigation'; +import variables from '@styles/variables'; import {searchInServer} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -81,10 +82,11 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen }); }, [cleanSearchTerm, selectedOptions, defaultOptionsModified]); - const {sections, headerMessage} = useMemo(() => { + const {sections, headerMessage, firstKeyForList} = useMemo(() => { const newSections: Section[] = []; + let firstKey = ''; if (!areOptionsInitialized) { - return {sections: [], headerMessage: undefined}; + return {sections: [], headerMessage: undefined, firstKeyForList: firstKey}; } newSections.push({ @@ -92,13 +94,16 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen data: chatOptions.recentReports, shouldShow: chatOptions.recentReports.length > 0, }); - + if (!firstKey) { + firstKey = chatOptions.recentReports.find((item) => item.isSelected)?.keyForList ?? ""; + } const areResultsFound = didScreenTransitionEnd && chatOptions.recentReports.length === 0; const message = areResultsFound ? translate('common.noResultsFound') : undefined; return { sections: newSections, headerMessage: message, + firstKeyForList: firstKey, }; }, [areOptionsInitialized, chatOptions.recentReports, didScreenTransitionEnd, reportAttributesDerived, translate]); @@ -159,6 +164,8 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen onSelectRow={handleParticipantSelection} isLoadingNewOptions={isLoadingNewOptions} showLoadingPlaceholder={showLoadingPlaceholder} + initiallyFocusedOptionKey={firstKeyForList} + getItemHeight={() => variables.optionRowHeight} /> ); } From 2bcf843937fb6b331144db569a06c3f20832e6c2 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 27 Aug 2025 21:22:09 +0530 Subject: [PATCH 30/50] add initiallyFocusedOptionKey --- .../Search/SearchFiltersParticipantsSelector.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index cc53b092b0e5..28faa0dfc7bb 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -12,6 +12,7 @@ import {filterAndOrderOptions, getValidOptions} from '@libs/OptionsListUtils'; import type {Option, Section} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; import Navigation from '@navigation/Navigation'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -90,10 +91,12 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: return filteredOptions; }, [defaultOptionsModified, cleanSearchTerm, selectedOptions]); - const {sections, headerMessage} = useMemo(() => { + const {sections, headerMessage, firstKeyForList} = useMemo(() => { const newSections: Section[] = []; + let firstKey = ''; + if (!areOptionsInitialized) { - return {sections: [], headerMessage: undefined}; + return {sections: [], headerMessage: undefined, firstKeyForList: firstKey}; } newSections.push({ @@ -101,12 +104,18 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: data: chatOptions.recentReports, shouldShow: chatOptions.recentReports.length > 0, }); + if (!firstKey) { + firstKey = chatOptions.recentReports.find((item) => item.isSelected)?.keyForList ?? ''; + } newSections.push({ title: '', data: chatOptions.personalDetails, shouldShow: chatOptions.personalDetails.length > 0, }); + if (!firstKey) { + firstKey = chatOptions.personalDetails.find((item) => item.isSelected)?.keyForList ?? ''; + } const noResultsFound = chatOptions.personalDetails.length === 0 && chatOptions.recentReports.length === 0 && !chatOptions.currentUserOption; const message = noResultsFound ? translate('common.noResultsFound') : undefined; @@ -114,6 +123,7 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: return { sections: newSections, headerMessage: message, + firstKeyForList: firstKey, }; }, [areOptionsInitialized, chatOptions, translate]); @@ -205,6 +215,8 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: onSelectRow={handleParticipantSelection} isLoadingNewOptions={isLoadingNewOptions} showLoadingPlaceholder={showLoadingPlaceholder} + initiallyFocusedOptionKey={firstKeyForList} + getItemHeight={() => variables.optionRowHeightCompact} /> ); } From f6c2e9b608a61adf08d5cf34d271b8d2d480d1b0 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 27 Aug 2025 21:23:05 +0530 Subject: [PATCH 31/50] add initiallyFocusedOptionKey --- src/components/Search/SearchMultipleSelectionPicker.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index 6740879c30f1..75bc5504fee1 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -8,6 +8,7 @@ import type {OptionData} from '@libs/ReportUtils'; import {sortOptionsWithEmptyValue} from '@libs/SearchQueryUtils'; import ROUTES from '@src/ROUTES'; import SearchFilterPageFooterButtons from './SearchFilterPageFooterButtons'; +import variables from '@styles/variables'; type SearchMultipleSelectionPickerItem = { name: string; @@ -56,6 +57,8 @@ function SearchMultipleSelectionPicker({items, initiallySelectedItems, pickerTit noResultsFound: isEmpty, }; }, [selectedItems, items, pickerTitle, debouncedSearchTerm, localeCompare]); + + const firstKey = sections?.at(0)?.data?.find((item) => item.isSelected)?.keyForList ?? null; const onSelectItem = useCallback( (item: Partial) => { @@ -103,6 +106,8 @@ function SearchMultipleSelectionPicker({items, initiallySelectedItems, pickerTit shouldShowTooltips canSelectMultiple ListItem={MultiSelectListItem} + initiallyFocusedOptionKey={firstKey} + getItemHeight={() => variables.optionRowHeightCompact} /> ); } From 50135d5af630ea180afc14244346263f42e7c858 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 27 Aug 2025 21:43:18 +0530 Subject: [PATCH 32/50] add new helper method getFirstSelectedItem --- src/libs/OptionsListUtils.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 232df2dad90e..6762ae92289f 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -2398,6 +2398,16 @@ function getFirstKeyForList(data?: Option[] | null) { return firstNonEmptyDataObj?.keyForList ? firstNonEmptyDataObj?.keyForList : ''; } +/** + * Helper method to get the `keyForList` for the first selected item + */ +function getFirstSelectedItem(data?: Option[] | null) { + if (!data?.length) { + return ''; + } + + return data.find((item) => item.isSelected)?.keyForList ?? ''; +} function getPersonalDetailSearchTerms(item: Partial) { if (item.accountID === currentUserAccountID) { return getCurrentUserSearchTerms(item); @@ -2741,6 +2751,7 @@ export { getCurrentUserSearchTerms, getEmptyOptions, getFirstKeyForList, + getFirstSelectedItem, getHeaderMessage, getHeaderMessageForNonUserList, getIOUConfirmationOptionsFromPayeePersonalDetail, From 58d0abacb43c10256af9a650bdefa405c85f39b5 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 27 Aug 2025 22:05:41 +0530 Subject: [PATCH 33/50] add initiallyFocusedOptionKey --- .../SearchFiltersCardPage.tsx | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx index f1f22b5bebeb..4813295d4e72 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx @@ -14,7 +14,9 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {openSearchFiltersCardPage, updateAdvancedFilters} from '@libs/actions/Search'; import type {CardFilterItem} from '@libs/CardFeedUtils'; import {buildCardFeedsData, buildCardsData, generateSelectedCards, getDomainFeedData, getSelectedCardsFromFeeds} from '@libs/CardFeedUtils'; +import {getFirstSelectedItem} from '@libs/OptionsListUtils'; import Navigation from '@navigation/Navigation'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -71,35 +73,42 @@ function SearchFiltersCardPage() { [debouncedSearchTerm, translate], ); - const sections = useMemo(() => { + const {sections, firstKeyForList} = useMemo(() => { + let firstKey = ''; if (searchAdvancedFiltersForm === undefined) { - return []; + return {sections: [], firstKeyForList: firstKey}; } const newSections = []; - // const selectedItems = [...cardFeedsSectionData.selected, ...individualCardsSectionData.selected, ...closedCardsSectionData.selected]; - // newSections.push({ - // title: undefined, - // data: selectedItems.filter(searchFunction), - // shouldShow: selectedItems.length > 0, - // }); newSections.push({ title: translate('search.filters.card.cardFeeds'), data: cardFeedsSectionData.filter(searchFunction), shouldShow: cardFeedsSectionData.length > 0, }); + if (!firstKey) { + firstKey = getFirstSelectedItem(cardFeedsSectionData); + } + newSections.push({ title: translate('search.filters.card.individualCards'), data: individualCardsSectionData.filter(searchFunction), shouldShow: individualCardsSectionData.length > 0, }); + if (!firstKey) { + firstKey = getFirstSelectedItem(individualCardsSectionData); + } + newSections.push({ title: translate('search.filters.card.closedCards'), data: closedCardsSectionData.filter(searchFunction), shouldShow: closedCardsSectionData.length > 0, }); - return newSections; + if (!firstKey) { + firstKey = getFirstSelectedItem(closedCardsSectionData); + } + + return {sections: newSections, firstKeyForList: firstKey}; }, [searchAdvancedFiltersForm, cardFeedsSectionData, individualCardsSectionData, closedCardsSectionData, searchFunction, translate]); const handleConfirmSelection = useCallback(() => { @@ -184,6 +193,8 @@ function SearchFiltersCardPage() { setSearchTerm(value); }} showLoadingPlaceholder={isLoadingOnyxValue(userCardListMetadata, workspaceCardFeedsMetadata, searchAdvancedFiltersFormMetadata) || !didScreenTransitionEnd} + initiallyFocusedOptionKey={firstKeyForList} + getItemHeight={() => variables.optionRowHeight} /> From 3dac380e0f49cccf3aea8bbd69c63368309f6db0 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 27 Aug 2025 22:13:24 +0530 Subject: [PATCH 34/50] stop separating selected and non selected options --- .../request/MoneyRequestAttendeeSelector.tsx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx index 482e8688d6cd..28bca43c504f 100644 --- a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx +++ b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx @@ -161,24 +161,6 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde const restOfRecents = [...chatOptions.recentReports].slice(5); const contactsWithRestOfRecents = [...restOfRecents, ...chatOptions.personalDetails]; - const formatResults = formatSectionsFromSearchTerm( - cleanSearchTerm, - filteredAttendees.map((attendee) => ({ - ...attendee, - reportID: CONST.DEFAULT_NUMBER_ID.toString(), - selected: true, - login: attendee.email, - ...getPersonalDetailByEmail(attendee.email), - })), - chatOptions.recentReports, - chatOptions.personalDetails, - personalDetails, - true, - undefined, - reportAttributesDerived, - ); - newSections.push(formatResults.section); - newSections.push({ title: translate('common.recents'), data: fiveRecents, From 0cb26bdf20495628bec8096e503efd5aa7a209ff Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 27 Aug 2025 22:57:51 +0530 Subject: [PATCH 35/50] stop separating selected and non selected options --- .../request/MoneyRequestAttendeeSelector.tsx | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx index 28bca43c504f..4187b9f8a2fe 100644 --- a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx +++ b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx @@ -137,17 +137,6 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde return newOptions; }, [areOptionsInitialized, defaultOptionsModified, cleanSearchTerm, isPaidGroupPolicy, attendees]); - // attendees who are not on expensify - const filteredAttendees = attendees - .filter((attendee) => attendee.accountID) - .map((attendee) => ({ - ...attendee, - reportID: CONST.DEFAULT_NUMBER_ID.toString(), - selected: true, - login: attendee.email, - ...getPersonalDetailByEmail(attendee.email), - })); - /** * Returns the sections needed for the OptionsSelector */ @@ -161,6 +150,39 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde const restOfRecents = [...chatOptions.recentReports].slice(5); const contactsWithRestOfRecents = [...restOfRecents, ...chatOptions.personalDetails]; + // Attendees whos data does not exist in chatOptions + const filteredAttendees = attendees + .filter( + (attendee) => + !fiveRecents.some((item) => attendee?.email === item.login || attendee?.accountID === item.accountID) && + !contactsWithRestOfRecents.some((item) => attendee?.email === item.login || attendee?.accountID === item.accountID), + ) + .map((attendee) => ({ + ...attendee, + reportID: CONST.DEFAULT_NUMBER_ID.toString(), + selected: true, + login: attendee.email, + ...getPersonalDetailByEmail(attendee.email), + })); + + const formatResults = formatSectionsFromSearchTerm( + cleanSearchTerm, + filteredAttendees.map((attendee) => ({ + ...attendee, + reportID: CONST.DEFAULT_NUMBER_ID.toString(), + selected: true, + login: attendee.email, + ...getPersonalDetailByEmail(attendee.email), + })), + chatOptions.recentReports, + chatOptions.personalDetails, + personalDetails, + true, + undefined, + reportAttributesDerived, + ); + newSections.push(formatResults.section); + newSections.push({ title: translate('common.recents'), data: fiveRecents, @@ -198,7 +220,6 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde }, [ areOptionsInitialized, didScreenTransitionEnd, - filteredAttendees, chatOptions.recentReports, chatOptions.personalDetails, chatOptions.userToInvite, From 0048c67bdd40ec7f81aa8380927ee3ddb1b4ed7a Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 27 Aug 2025 23:11:07 +0530 Subject: [PATCH 36/50] add initiallyFocusedOptionKey --- .../request/MoneyRequestAttendeeSelector.tsx | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx index 4187b9f8a2fe..758ef437f181 100644 --- a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx +++ b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx @@ -24,6 +24,7 @@ import { formatSectionsFromSearchTerm, getAttendeeOptions, getEmptyOptions, + getFirstSelectedItem, getHeaderMessage, getParticipantsOption, getPersonalDetailSearchTerms, @@ -33,6 +34,7 @@ import { } from '@libs/OptionsListUtils'; import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; import {isPaidGroupPolicy as isPaidGroupPolicyFn} from '@libs/PolicyUtils'; +import variables from '@styles/variables'; import {searchInServer} from '@userActions/Report'; import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; @@ -140,10 +142,12 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde /** * Returns the sections needed for the OptionsSelector */ - const [sections, header] = useMemo(() => { + const {sections, header, firstKeyForList} = useMemo(() => { const newSections: Array> = []; + let firstKey = ''; + if (!areOptionsInitialized || !didScreenTransitionEnd) { - return [newSections, '']; + return {sections: newSections, header: '', firstKeyForList: firstKey}; } const fiveRecents = [...chatOptions.recentReports].slice(0, 5); @@ -164,7 +168,6 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde login: attendee.email, ...getPersonalDetailByEmail(attendee.email), })); - const formatResults = formatSectionsFromSearchTerm( cleanSearchTerm, filteredAttendees.map((attendee) => ({ @@ -182,31 +185,45 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde reportAttributesDerived, ); newSections.push(formatResults.section); + if (!firstKey) { + firstKey = getFirstSelectedItem(formatResults.section?.data); + } newSections.push({ title: translate('common.recents'), data: fiveRecents, shouldShow: fiveRecents.length > 0, }); + if (!firstKey) { + firstKey = getFirstSelectedItem(fiveRecents); + } newSections.push({ title: translate('common.contacts'), data: contactsWithRestOfRecents, shouldShow: contactsWithRestOfRecents.length > 0, }); + if (!firstKey) { + firstKey = getFirstSelectedItem(contactsWithRestOfRecents); + } if ( chatOptions.userToInvite && !isCurrentUser({...chatOptions.userToInvite, accountID: chatOptions.userToInvite?.accountID ?? CONST.DEFAULT_NUMBER_ID, status: chatOptions.userToInvite?.status ?? undefined}) ) { + const modifiedUserToInvite = [chatOptions.userToInvite].map((participant) => { + const isPolicyExpenseChat = participant?.isPolicyExpenseChat ?? false; + return isPolicyExpenseChat ? getPolicyExpenseReportOption(participant, reportAttributesDerived) : getParticipantsOption(participant, personalDetails); + }); + newSections.push({ title: undefined, - data: [chatOptions.userToInvite].map((participant) => { - const isPolicyExpenseChat = participant?.isPolicyExpenseChat ?? false; - return isPolicyExpenseChat ? getPolicyExpenseReportOption(participant, reportAttributesDerived) : getParticipantsOption(participant, personalDetails); - }), + data: modifiedUserToInvite, shouldShow: true, }); + if (!firstKey) { + firstKey = getFirstSelectedItem(modifiedUserToInvite); + } } const headerMessage = getHeaderMessage( @@ -216,7 +233,7 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde attendees.some((attendee) => getPersonalDetailSearchTerms(attendee).join(' ').toLowerCase().includes(cleanSearchTerm)), ); - return [newSections, headerMessage]; + return {sections: newSections, header: headerMessage, firstKeyForList: firstKey}; }, [ areOptionsInitialized, didScreenTransitionEnd, @@ -342,6 +359,8 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde canSelectMultiple isLoadingNewOptions={!!isSearchingForReports} shouldShowListEmptyContent={shouldShowListEmptyContent} + initiallyFocusedOptionKey={firstKeyForList} + getItemHeight={() => variables.optionRowHeight} /> ); } From 638d154e9a93123d738f5059fa4f0e82d7b01e9c Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 27 Aug 2025 23:12:41 +0530 Subject: [PATCH 37/50] stop separating selected and non selected options --- src/pages/NewChatPage.tsx | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 45c217b1c4a7..4d70065ae558 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -28,7 +28,15 @@ 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, getFirstKeyForList, getHeaderMessage, getPersonalDetailSearchTerms, getUserToInviteOption, getValidOptions} from '@libs/OptionsListUtils'; +import { + filterAndOrderOptions, + formatSectionsFromSearchTerm, + getFirstKeyForList, + getHeaderMessage, + getPersonalDetailSearchTerms, + getUserToInviteOption, + getValidOptions, +} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -173,6 +181,7 @@ function NewChatPage(_: unknown, ref: React.Ref) { const personalData = useCurrentUserPersonalDetails(); const {top} = useSafeAreaInsets(); const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true}); + const [reportAttributesDerived] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true, selector: (val) => val?.reports}); const selectionListRef = useRef(null); const {singleExecution} = useSingleExecution(); @@ -181,12 +190,37 @@ function NewChatPage(_: unknown, ref: React.Ref) { focus: selectionListRef.current?.focusTextInput, })); - const {headerMessage, searchTerm, setSearchTerm, selectedOptions, setSelectedOptions, recentReports, personalDetails, userToInvite, areOptionsInitialized} = useOptions(); + const {headerMessage, searchTerm, setSearchTerm, debouncedSearchTerm, selectedOptions, setSelectedOptions, recentReports, personalDetails, userToInvite, areOptionsInitialized} = + useOptions(); const [sections, firstKeyForList] = useMemo(() => { const sectionsList: Section[] = []; let firstKey = ''; + // Attendees whos data does not exist in chatOptions + const filteredSelectedOptions = selectedOptions + .filter((participent) => !recentReports.some((item) => participent.login === item.login) && !personalDetails.some((item) => participent.login === item.login)) + .map((participent) => ({ + ...participent, + reportID: CONST.DEFAULT_NUMBER_ID.toString(), + selected: true, + login: participent.login, + })); + + const formatResults = formatSectionsFromSearchTerm( + debouncedSearchTerm, + filteredSelectedOptions as OptionData[], + recentReports, + personalDetails, + undefined, + undefined, + undefined, + reportAttributesDerived, + ); + sectionsList.push(formatResults.section); + if (!firstKey) { + firstKey = getFirstKeyForList(formatResults.section.data); + } sectionsList.push({ title: translate('common.recents'), data: selectedOptions.length ? recentReports.filter((option) => !option.isSelfDM) : recentReports, From 30bfa4a2d4c62aa7bf3e281ba3ffd761d7f1b149 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 27 Aug 2025 23:25:33 +0530 Subject: [PATCH 38/50] stop separating selected and non selected options --- src/pages/workspace/WorkspaceInvitePage.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index d1446db16e68..74b36a16ac5d 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -23,7 +23,7 @@ import HttpUtils from '@libs/HttpUtils'; import {appendCountryCode} from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import {filterAndOrderOptions, formatMemberForList, getHeaderMessage, getMemberInviteOptions} from '@libs/OptionsListUtils'; +import {filterAndOrderOptions, formatMemberForList, getHeaderMessage, getMemberInviteOptions, getSearchValueForPhoneOrEmail} from '@libs/OptionsListUtils'; import type {MemberForList} from '@libs/OptionsListUtils'; import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from '@libs/PhoneNumber'; import {getIneligibleInvitees, getMemberAccountIDsForWorkspace, goBackFromInvalidPolicy} from '@libs/PolicyUtils'; @@ -176,6 +176,23 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { ); const personalDetailsFormatted = personalDetailsModified.map((item) => formatMemberForList(item)); + // Filter all options that is a part of the search term and are not part of personalDetails + let filterSelectedOptions = selectedOptions.filter((participent) => !personalDetails.some((item) => participent.login === item.login)); + if (debouncedSearchTerm !== '') { + filterSelectedOptions = filterSelectedOptions.filter((option) => { + const searchValue = getSearchValueForPhoneOrEmail(debouncedSearchTerm); + + const isPartOfSearchTerm = !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); + return isPartOfSearchTerm; + }); + } + + sectionsArr.push({ + title: undefined, + data: filterSelectedOptions, + shouldShow: true, + }); + sectionsArr.push({ title: translate('common.contacts'), data: personalDetailsFormatted, From b583ed6032c6ab8965ccba570f70dc449d6daaa9 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 27 Aug 2025 23:32:20 +0530 Subject: [PATCH 39/50] add initiallyFocusedOptionKey --- src/pages/workspace/WorkspaceInvitePage.tsx | 25 +++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 74b36a16ac5d..859881f7622e 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -23,7 +23,7 @@ import HttpUtils from '@libs/HttpUtils'; import {appendCountryCode} from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import {filterAndOrderOptions, formatMemberForList, getHeaderMessage, getMemberInviteOptions, getSearchValueForPhoneOrEmail} from '@libs/OptionsListUtils'; +import {filterAndOrderOptions, formatMemberForList, getFirstSelectedItem, getHeaderMessage, getMemberInviteOptions, getSearchValueForPhoneOrEmail} from '@libs/OptionsListUtils'; import type {MemberForList} from '@libs/OptionsListUtils'; import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from '@libs/PhoneNumber'; import {getIneligibleInvitees, getMemberAccountIDsForWorkspace, goBackFromInvalidPolicy} from '@libs/PolicyUtils'; @@ -39,6 +39,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AccessOrNotFoundWrapper from './AccessOrNotFoundWrapper'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; +import variables from '@styles/variables'; type MembersSection = SectionListData>; @@ -163,11 +164,12 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change }, [options.personalDetails, policy?.employeeList, betas, debouncedSearchTerm, excludedUsers, areOptionsInitialized, inviteOptions.personalDetails, inviteOptions.userToInvite]); - const sections: MembersSection[] = useMemo(() => { + const {sections, firstKeyForList} = useMemo(() => { const sectionsArr: MembersSection[] = []; + let firstKey = ''; if (!areOptionsInitialized) { - return []; + return {sections:[],firstKeyForList:firstKey}; } const selectedLogins = selectedOptions.map(({login}) => login); @@ -192,16 +194,22 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { data: filterSelectedOptions, shouldShow: true, }); - + if (!firstKey) { + firstKey = getFirstSelectedItem(filterSelectedOptions); + } + sectionsArr.push({ title: translate('common.contacts'), data: personalDetailsFormatted, shouldShow: !isEmptyObject(personalDetailsFormatted), }); - + if (!firstKey) { + firstKey = getFirstSelectedItem(personalDetailsFormatted); + } + Object.values(usersToInvite).forEach((userToInvite) => { const hasUnselectedUserToInvite = !selectedLogins.some((selectedLogin) => selectedLogin === userToInvite.login); - + if (hasUnselectedUserToInvite) { sectionsArr.push({ title: undefined, @@ -211,7 +219,8 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { } }); - return sectionsArr; + + return {sections:sectionsArr,firstKeyForList:firstKey}; }, [areOptionsInitialized, selectedOptions, personalDetails, translate, usersToInvite]); const toggleOption = (option: MemberForList) => { @@ -330,6 +339,8 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { footerContent={footerContent} isLoadingNewOptions={!!isSearchingForReports} addBottomSafeAreaPadding + initiallyFocusedOptionKey={firstKeyForList} + getItemHeight={() => variables.optionRowHeight} /> From 21b29c4c1eb7702e32f0225e366b1d0fbce09f60 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 27 Aug 2025 23:35:47 +0530 Subject: [PATCH 40/50] prettier --- .../Search/SearchFiltersChatsSelector.tsx | 5 +++-- .../Search/SearchFiltersParticipantsSelector.tsx | 6 +++--- .../Search/SearchMultipleSelectionPicker.tsx | 7 ++++--- src/pages/workspace/WorkspaceInvitePage.tsx | 13 ++++++------- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx index e358ed686019..bd42a399cf1b 100644 --- a/src/components/Search/SearchFiltersChatsSelector.tsx +++ b/src/components/Search/SearchFiltersChatsSelector.tsx @@ -9,7 +9,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useScreenWrapperTransitionStatus from '@hooks/useScreenWrapperTransitionStatus'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; -import {createOptionFromReport, filterAndOrderOptions, getAlternateText, getSearchOptions} from '@libs/OptionsListUtils'; +import {createOptionFromReport, filterAndOrderOptions, getAlternateText, getFirstSelectedItem, getSearchOptions} from '@libs/OptionsListUtils'; import type {Option, Section} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; import Navigation from '@navigation/Navigation'; @@ -79,6 +79,7 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen return filterAndOrderOptions(defaultOptionsModified, cleanSearchTerm, { selectedOptions, excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, + exc, }); }, [cleanSearchTerm, selectedOptions, defaultOptionsModified]); @@ -95,7 +96,7 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen shouldShow: chatOptions.recentReports.length > 0, }); if (!firstKey) { - firstKey = chatOptions.recentReports.find((item) => item.isSelected)?.keyForList ?? ""; + firstKey = getFirstSelectedItem(chatOptions.recentReports); } const areResultsFound = didScreenTransitionEnd && chatOptions.recentReports.length === 0; const message = areResultsFound ? translate('common.noResultsFound') : undefined; diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index 28faa0dfc7bb..7e1f5f5f0380 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -8,7 +8,7 @@ import useOnyx from '@hooks/useOnyx'; import useScreenWrapperTransitionStatus from '@hooks/useScreenWrapperTransitionStatus'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import memoize from '@libs/memoize'; -import {filterAndOrderOptions, getValidOptions} from '@libs/OptionsListUtils'; +import {filterAndOrderOptions, getFirstSelectedItem, getValidOptions} from '@libs/OptionsListUtils'; import type {Option, Section} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; import Navigation from '@navigation/Navigation'; @@ -105,7 +105,7 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: shouldShow: chatOptions.recentReports.length > 0, }); if (!firstKey) { - firstKey = chatOptions.recentReports.find((item) => item.isSelected)?.keyForList ?? ''; + firstKey = getFirstSelectedItem(chatOptions.recentReports); } newSections.push({ @@ -114,7 +114,7 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: shouldShow: chatOptions.personalDetails.length > 0, }); if (!firstKey) { - firstKey = chatOptions.personalDetails.find((item) => item.isSelected)?.keyForList ?? ''; + firstKey = getFirstSelectedItem(chatOptions.personalDetails); } const noResultsFound = chatOptions.personalDetails.length === 0 && chatOptions.recentReports.length === 0 && !chatOptions.currentUserOption; diff --git a/src/components/Search/SearchMultipleSelectionPicker.tsx b/src/components/Search/SearchMultipleSelectionPicker.tsx index 75bc5504fee1..48ff1bf726c0 100644 --- a/src/components/Search/SearchMultipleSelectionPicker.tsx +++ b/src/components/Search/SearchMultipleSelectionPicker.tsx @@ -4,11 +4,12 @@ import MultiSelectListItem from '@components/SelectionList/MultiSelectListItem'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; +import {getFirstSelectedItem} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; import {sortOptionsWithEmptyValue} from '@libs/SearchQueryUtils'; +import variables from '@styles/variables'; import ROUTES from '@src/ROUTES'; import SearchFilterPageFooterButtons from './SearchFilterPageFooterButtons'; -import variables from '@styles/variables'; type SearchMultipleSelectionPickerItem = { name: string; @@ -57,8 +58,8 @@ function SearchMultipleSelectionPicker({items, initiallySelectedItems, pickerTit noResultsFound: isEmpty, }; }, [selectedItems, items, pickerTitle, debouncedSearchTerm, localeCompare]); - - const firstKey = sections?.at(0)?.data?.find((item) => item.isSelected)?.keyForList ?? null; + + const firstKey = getFirstSelectedItem(sections?.at(0)?.data); const onSelectItem = useCallback( (item: Partial) => { diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 859881f7622e..acb3b5f4c0e0 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -29,6 +29,7 @@ import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from '@libs/PhoneNumber'; import {getIneligibleInvitees, getMemberAccountIDsForWorkspace, goBackFromInvalidPolicy} from '@libs/PolicyUtils'; import type {OptionData} from '@libs/ReportUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -39,7 +40,6 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AccessOrNotFoundWrapper from './AccessOrNotFoundWrapper'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; -import variables from '@styles/variables'; type MembersSection = SectionListData>; @@ -169,7 +169,7 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { let firstKey = ''; if (!areOptionsInitialized) { - return {sections:[],firstKeyForList:firstKey}; + return {sections: [], firstKeyForList: firstKey}; } const selectedLogins = selectedOptions.map(({login}) => login); @@ -197,7 +197,7 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { if (!firstKey) { firstKey = getFirstSelectedItem(filterSelectedOptions); } - + sectionsArr.push({ title: translate('common.contacts'), data: personalDetailsFormatted, @@ -206,10 +206,10 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { if (!firstKey) { firstKey = getFirstSelectedItem(personalDetailsFormatted); } - + Object.values(usersToInvite).forEach((userToInvite) => { const hasUnselectedUserToInvite = !selectedLogins.some((selectedLogin) => selectedLogin === userToInvite.login); - + if (hasUnselectedUserToInvite) { sectionsArr.push({ title: undefined, @@ -219,8 +219,7 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { } }); - - return {sections:sectionsArr,firstKeyForList:firstKey}; + return {sections: sectionsArr, firstKeyForList: firstKey}; }, [areOptionsInitialized, selectedOptions, personalDetails, translate, usersToInvite]); const toggleOption = (option: MemberForList) => { From 9556171fda8844a362ca28a3dd55d504e00db520 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Thu, 28 Aug 2025 17:30:15 +0530 Subject: [PATCH 41/50] lint/prettier --- src/components/Search/SearchFiltersChatsSelector.tsx | 3 +-- src/components/SelectionList/BaseSelectionList.tsx | 1 + src/pages/NewChatPage.tsx | 4 ++-- src/pages/iou/request/MoneyRequestAttendeeSelector.tsx | 10 +++++++--- src/pages/workspace/WorkspaceInvitePage.tsx | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx index bd42a399cf1b..da13ab320937 100644 --- a/src/components/Search/SearchFiltersChatsSelector.tsx +++ b/src/components/Search/SearchFiltersChatsSelector.tsx @@ -79,7 +79,6 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen return filterAndOrderOptions(defaultOptionsModified, cleanSearchTerm, { selectedOptions, excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, - exc, }); }, [cleanSearchTerm, selectedOptions, defaultOptionsModified]); @@ -106,7 +105,7 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen headerMessage: message, firstKeyForList: firstKey, }; - }, [areOptionsInitialized, chatOptions.recentReports, didScreenTransitionEnd, reportAttributesDerived, translate]); + }, [areOptionsInitialized, chatOptions.recentReports, didScreenTransitionEnd, translate]); useEffect(() => { searchInServer(debouncedSearchTerm.trim()); diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 97b9d6b19e7c..2d15b3f5fccd 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -855,6 +855,7 @@ function BaseSelectionList( flattenedSections.allOptions, isItemSelected, slicedSections.length, + isInitialSectionListRender, ]); useEffect( diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 4d70065ae558..9bcca1be7c5e 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -197,7 +197,7 @@ function NewChatPage(_: unknown, ref: React.Ref) { const sectionsList: Section[] = []; let firstKey = ''; - // Attendees whos data does not exist in chatOptions + // Participents whose data does not exist in chatOptions const filteredSelectedOptions = selectedOptions .filter((participent) => !recentReports.some((item) => participent.login === item.login) && !personalDetails.some((item) => participent.login === item.login)) .map((participent) => ({ @@ -251,7 +251,7 @@ function NewChatPage(_: unknown, ref: React.Ref) { } return [sectionsList, firstKey]; - }, [selectedOptions, recentReports, personalDetails, translate, userToInvite]); + }, [selectedOptions, debouncedSearchTerm, reportAttributesDerived, recentReports, personalDetails, translate, userToInvite]); /** * Removes a selected option from list if already selected. If not already selected add this option to the list. diff --git a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx index 758ef437f181..ab722f75ccd5 100644 --- a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx +++ b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx @@ -105,10 +105,14 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde return { ...defaultOptions, recentReports: defaultOptions.recentReports.map((item) => - attendees.some((attendee) => attendee?.email === item.login || attendee?.accountID === item.accountID) ? {...item, isSelected: true} : item, + attendees.some((attendee) => (attendee?.email && attendee.email === item.login) || (attendee?.accountID && attendee.accountID === item.accountID)) + ? {...item, isSelected: true} + : item, ), personalDetails: defaultOptions.personalDetails.map((item) => - attendees.some((attendee) => attendee?.email === item.login || attendee?.accountID === item.accountID) ? {...item, isSelected: true} : item, + attendees.some((attendee) => (attendee?.email && attendee.email === item.login) || (attendee?.accountID && attendee.accountID === item.accountID)) + ? {...item, isSelected: true} + : item, ), }; }, [defaultOptions, attendees]); @@ -154,7 +158,7 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde const restOfRecents = [...chatOptions.recentReports].slice(5); const contactsWithRestOfRecents = [...restOfRecents, ...chatOptions.personalDetails]; - // Attendees whos data does not exist in chatOptions + // Attendees whose data does not exist in chatOptions const filteredAttendees = attendees .filter( (attendee) => diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index acb3b5f4c0e0..979ce98f7a34 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -220,7 +220,7 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { }); return {sections: sectionsArr, firstKeyForList: firstKey}; - }, [areOptionsInitialized, selectedOptions, personalDetails, translate, usersToInvite]); + }, [debouncedSearchTerm, areOptionsInitialized, selectedOptions, personalDetails, translate, usersToInvite]); const toggleOption = (option: MemberForList) => { clearErrors(route.params.policyID); From 673108893f596b8a33307d5a7266398dbb08da61 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Thu, 28 Aug 2025 17:35:20 +0530 Subject: [PATCH 42/50] lint/prettier --- src/pages/workspace/WorkspaceInvitePage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 979ce98f7a34..6e73285d0113 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -178,8 +178,8 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { ); const personalDetailsFormatted = personalDetailsModified.map((item) => formatMemberForList(item)); - // Filter all options that is a part of the search term and are not part of personalDetails - let filterSelectedOptions = selectedOptions.filter((participent) => !personalDetails.some((item) => participent.login === item.login)); + // Filter selected participants which are part of personalDetails + let filterSelectedOptions = selectedOptions.filter((participant) => !personalDetails.some((item) => participant.login === item.login)); if (debouncedSearchTerm !== '') { filterSelectedOptions = filterSelectedOptions.filter((option) => { const searchValue = getSearchValueForPhoneOrEmail(debouncedSearchTerm); From 90905506153f539f6918140f9a41580448fabd25 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Thu, 28 Aug 2025 17:41:48 +0530 Subject: [PATCH 43/50] prettier --- src/components/SelectionList/BaseSelectionList.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 2d15b3f5fccd..04ffc34bf0f2 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -831,8 +831,9 @@ function BaseSelectionList( } // Avoid clearing focus on initial render - if (isInitialSectionListRender) return; - + if (isInitialSectionListRender) { + return; + } // Remove the focus if the search input is empty and prev search input not empty or selected options length is changed (and allOptions length remains the same) // else focus on the first non disabled item const newSelectedIndex = From a30f7c744149c994fe248f9d4fbc901f8702ee6c Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Thu, 28 Aug 2025 17:46:49 +0530 Subject: [PATCH 44/50] lint --- src/pages/NewChatPage.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 9bcca1be7c5e..8dbc2118bc51 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -197,14 +197,14 @@ function NewChatPage(_: unknown, ref: React.Ref) { const sectionsList: Section[] = []; let firstKey = ''; - // Participents whose data does not exist in chatOptions + // Participants whose data does not exist in chatOptions const filteredSelectedOptions = selectedOptions - .filter((participent) => !recentReports.some((item) => participent.login === item.login) && !personalDetails.some((item) => participent.login === item.login)) - .map((participent) => ({ - ...participent, + .filter((participant) => !recentReports.some((item) => participant.login === item.login) && !personalDetails.some((item) => participant.login === item.login)) + .map((participant) => ({ + ...participant, reportID: CONST.DEFAULT_NUMBER_ID.toString(), selected: true, - login: participent.login, + login: participant.login, })); const formatResults = formatSectionsFromSearchTerm( From 38c4e56cfd729bc347588fef002b2c7d1db54932 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Thu, 28 Aug 2025 18:33:05 +0530 Subject: [PATCH 45/50] add initiallyFocusedOptionKey --- src/hooks/useWorkspaceList.ts | 7 +++++-- src/libs/OptionsListUtils.ts | 3 ++- .../SearchFiltersWorkspacePage.tsx | 5 ++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/hooks/useWorkspaceList.ts b/src/hooks/useWorkspaceList.ts index c7289c4f4d18..e66abfba2a2d 100644 --- a/src/hooks/useWorkspaceList.ts +++ b/src/hooks/useWorkspaceList.ts @@ -3,6 +3,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import type {ListItem, SectionListDataType} from '@components/SelectionList/types'; +import {getFirstSelectedItem} from '@libs/OptionsListUtils'; import {isPolicyAdmin, shouldShowPolicy} from '@libs/PolicyUtils'; import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; import tokenizedSearch from '@libs/tokenizedSearch'; @@ -65,7 +66,7 @@ function useWorkspaceList({policies, currentUserLogin, selectedPolicyIDs, search [searchTerm, usersWorkspaces, localeCompare], ); - const sections = useMemo(() => { + const {sections, firstKeyForList} = useMemo(() => { const options: Array> = [ { data: filteredAndSortedUserWorkspaces, @@ -73,7 +74,8 @@ function useWorkspaceList({policies, currentUserLogin, selectedPolicyIDs, search indexOffset: 1, }, ]; - return options; + const firstKeyForList = getFirstSelectedItem(filteredAndSortedUserWorkspaces); + return {sections: options, firstKeyForList: firstKeyForList}; }, [filteredAndSortedUserWorkspaces]); const shouldShowNoResultsFoundMessage = filteredAndSortedUserWorkspaces.length === 0 && usersWorkspaces.length; @@ -83,6 +85,7 @@ function useWorkspaceList({policies, currentUserLogin, selectedPolicyIDs, search sections, shouldShowNoResultsFoundMessage, shouldShowSearchInput, + firstKeyForList, }; } diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 6762ae92289f..202d327601b6 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -8,6 +8,7 @@ import Onyx from 'react-native-onyx'; import type {SetNonNullable} from 'type-fest'; import {FallbackAvatar} from '@components/Icon/Expensicons'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; +import {WorkspaceListItem} from '@hooks/useWorkspaceList'; import type {PolicyTagList} from '@pages/workspace/tags/types'; import type {IOUAction} from '@src/CONST'; import CONST from '@src/CONST'; @@ -2401,7 +2402,7 @@ function getFirstKeyForList(data?: Option[] | null) { /** * Helper method to get the `keyForList` for the first selected item */ -function getFirstSelectedItem(data?: Option[] | null) { +function getFirstSelectedItem(data?: Option[] | WorkspaceListItem[] | null) { if (!data?.length) { return ''; } diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersWorkspacePage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersWorkspacePage.tsx index 6aedf0bfa8de..f158afffa715 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersWorkspacePage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersWorkspacePage.tsx @@ -14,6 +14,7 @@ import type {WorkspaceListItem} from '@hooks/useWorkspaceList'; import useWorkspaceList from '@hooks/useWorkspaceList'; import {updateAdvancedFilters} from '@libs/actions/Search'; import Navigation from '@libs/Navigation/Navigation'; +import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; @@ -39,7 +40,7 @@ function SearchFiltersWorkspacePage() { const [selectedOptions, setSelectedOptions] = useState(() => (searchAdvancedFiltersForm?.policyID ? Array.from(searchAdvancedFiltersForm?.policyID) : [])); - const {sections, shouldShowNoResultsFoundMessage, shouldShowSearchInput} = useWorkspaceList({ + const {sections, shouldShowNoResultsFoundMessage, shouldShowSearchInput, firstKeyForList} = useWorkspaceList({ policies, currentUserLogin, shouldShowPendingDeletePolicy: false, @@ -110,6 +111,8 @@ function SearchFiltersWorkspacePage() { resetChanges={resetChanges} /> } + initiallyFocusedOptionKey={firstKeyForList} + getItemHeight={() => variables.optionRowHeight} /> )} From 11a6c0f4550d4be931b7058805e66d4ee1b20775 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Thu, 28 Aug 2025 20:09:08 +0530 Subject: [PATCH 46/50] Stop scrolling to top when selecting an item --- src/pages/NewChatPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 8dbc2118bc51..b8875403eae5 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -266,7 +266,6 @@ function NewChatPage(_: unknown, ref: React.Ref) { newSelectedOptions = reject(selectedOptions, (selectedOption) => selectedOption.login === option.login); } else { newSelectedOptions = [...selectedOptions, {...option, isSelected: true, selected: true, reportID: option.reportID}]; - selectionListRef?.current?.scrollToIndex(0, true); } selectionListRef?.current?.clearInputAfterSelect?.(); From 0976439ab7965edc0b3d628765aa83906451cd0d Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Thu, 28 Aug 2025 20:16:46 +0530 Subject: [PATCH 47/50] Optimization + pass initialNumToRender to prevent flicker after list is rendered --- src/components/Search/SearchFiltersChatsSelector.tsx | 1 + .../Search/SearchFiltersParticipantsSelector.tsx | 11 ++++------- src/hooks/useWorkspaceList.ts | 4 ++-- src/libs/OptionsListUtils.ts | 2 +- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx index da13ab320937..d1be1852fe82 100644 --- a/src/components/Search/SearchFiltersChatsSelector.tsx +++ b/src/components/Search/SearchFiltersChatsSelector.tsx @@ -166,6 +166,7 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen showLoadingPlaceholder={showLoadingPlaceholder} initiallyFocusedOptionKey={firstKeyForList} getItemHeight={() => variables.optionRowHeight} + initialNumToRender={firstKeyForList ? CONST.MAX_SELECTION_LIST_PAGE_LENGTH : undefined} /> ); } diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index 7e1f5f5f0380..a90d0cda2776 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -67,16 +67,12 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: }, ); }, [areOptionsInitialized, options.personalDetails, options.reports]); - + const selectedAccountIDsSet = new Set(selectedOptions.map(({accountID}) => accountID)); const defaultOptionsModified = useMemo(() => { return { ...defaultOptions, - recentReports: defaultOptions.recentReports.map((item) => - selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true} : item, - ), - personalDetails: defaultOptions.personalDetails.map((item) => - selectedOptions.some((selectedOption) => selectedOption.accountID === item.accountID) ? {...item, isSelected: true} : item, - ), + recentReports: defaultOptions.recentReports.map((item) => (selectedAccountIDsSet.has(item.accountID) ? {...item, isSelected: true} : item)), + personalDetails: defaultOptions.personalDetails.map((item) => (selectedAccountIDsSet.has(item.accountID) ? {...item, isSelected: true} : item)), }; }, [defaultOptions, selectedOptions]); @@ -217,6 +213,7 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: showLoadingPlaceholder={showLoadingPlaceholder} initiallyFocusedOptionKey={firstKeyForList} getItemHeight={() => variables.optionRowHeightCompact} + initialNumToRender={firstKeyForList ? CONST.MAX_SELECTION_LIST_PAGE_LENGTH : undefined} /> ); } diff --git a/src/hooks/useWorkspaceList.ts b/src/hooks/useWorkspaceList.ts index e66abfba2a2d..ccb81cf31cc0 100644 --- a/src/hooks/useWorkspaceList.ts +++ b/src/hooks/useWorkspaceList.ts @@ -74,8 +74,8 @@ function useWorkspaceList({policies, currentUserLogin, selectedPolicyIDs, search indexOffset: 1, }, ]; - const firstKeyForList = getFirstSelectedItem(filteredAndSortedUserWorkspaces); - return {sections: options, firstKeyForList: firstKeyForList}; + const firstKey = getFirstSelectedItem(filteredAndSortedUserWorkspaces); + return {sections: options, firstKeyForList: firstKey}; }, [filteredAndSortedUserWorkspaces]); const shouldShowNoResultsFoundMessage = filteredAndSortedUserWorkspaces.length === 0 && usersWorkspaces.length; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 202d327601b6..65b7efdb73bc 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -8,7 +8,7 @@ import Onyx from 'react-native-onyx'; import type {SetNonNullable} from 'type-fest'; import {FallbackAvatar} from '@components/Icon/Expensicons'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; -import {WorkspaceListItem} from '@hooks/useWorkspaceList'; +import type {WorkspaceListItem} from '@hooks/useWorkspaceList'; import type {PolicyTagList} from '@pages/workspace/tags/types'; import type {IOUAction} from '@src/CONST'; import CONST from '@src/CONST'; From e62e3943031762e457a41ab48c000812b457e4dd Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Mon, 1 Sep 2025 15:19:03 +0530 Subject: [PATCH 48/50] Remove flaky check and lint --- .../Search/SearchFiltersParticipantsSelector.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index 2860121c1df4..6540d9a352dc 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -67,14 +67,14 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: }, ); }, [areOptionsInitialized, options.personalDetails, options.reports]); - const selectedAccountIDsSet = new Set(selectedOptions.map(({accountID}) => accountID)); + const selectedAccountIDsSet = useMemo(() => new Set(selectedOptions.map(({accountID}) => accountID)), [selectedOptions]); const defaultOptionsModified = useMemo(() => { return { ...defaultOptions, recentReports: defaultOptions.recentReports.map((item) => (selectedAccountIDsSet.has(item.accountID) ? {...item, isSelected: true} : item)), personalDetails: defaultOptions.personalDetails.map((item) => (selectedAccountIDsSet.has(item.accountID) ? {...item, isSelected: true} : item)), }; - }, [defaultOptions, selectedOptions]); + }, [defaultOptions, selectedAccountIDsSet]); const chatOptions = useMemo(() => { const filteredOptions = filterAndOrderOptions(defaultOptionsModified, cleanSearchTerm, { @@ -164,10 +164,6 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: return true; } - if (selectedOption.reportID && selectedOption.reportID === option?.reportID) { - return true; - } - return false; }); From 703528ea0d9716aa0ee6c30c840b71b73498f815 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Tue, 2 Sep 2025 13:29:09 +0530 Subject: [PATCH 49/50] undo unwanted changes --- src/components/Search/SearchFiltersChatsSelector.tsx | 1 - src/components/Search/SearchFiltersParticipantsSelector.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx index 99afe9f85221..8daf55b0548b 100644 --- a/src/components/Search/SearchFiltersChatsSelector.tsx +++ b/src/components/Search/SearchFiltersChatsSelector.tsx @@ -166,7 +166,6 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen showLoadingPlaceholder={showLoadingPlaceholder} initiallyFocusedOptionKey={firstKeyForList} getItemHeight={() => variables.optionRowHeight} - initialNumToRender={firstKeyForList ? CONST.MAX_SELECTION_LIST_PAGE_LENGTH : undefined} /> ); } diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index 6540d9a352dc..29f76a822184 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -209,7 +209,6 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: showLoadingPlaceholder={showLoadingPlaceholder} initiallyFocusedOptionKey={firstKeyForList} getItemHeight={() => variables.optionRowHeightCompact} - initialNumToRender={firstKeyForList ? CONST.MAX_SELECTION_LIST_PAGE_LENGTH : undefined} /> ); } From d804b21ebe8f6ca563f776a5dbf588b762ea269d Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Tue, 2 Sep 2025 13:30:15 +0530 Subject: [PATCH 50/50] Prevent animation for initial scroll --- src/components/SelectionList/BaseSelectionList.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index c60e2633a4bf..30b71028d405 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -166,6 +166,7 @@ function BaseSelectionList({ const {isKeyboardShown} = useKeyboardState(); const [itemsToHighlight, setItemsToHighlight] = useState | null>(null); const itemFocusTimeoutRef = useRef(null); + const shouldPreventScrollAnimationRef = useRef(false); const isItemSelected = useCallback( (item: TItem) => item.isSelected ?? ((isSelected?.(item) ?? selectedItems.includes(item.keyForList ?? '')) && canSelectMultiple), [isSelected, selectedItems, canSelectMultiple], @@ -431,7 +432,8 @@ function BaseSelectionList({ onArrowFocus(focusedItem); } if (shouldScrollToFocusedIndex) { - (shouldDebounceScrolling ? debouncedScrollToIndex : scrollToIndex)(index, true); + (shouldDebounceScrolling ? debouncedScrollToIndex : scrollToIndex)(index, !shouldPreventScrollAnimationRef.current && true); + shouldPreventScrollAnimationRef.current = false; } }, ...(!hasKeyBeenPressed.current && {setHasKeyBeenPressed}), @@ -453,6 +455,7 @@ function BaseSelectionList({ if (selectedItemIndex === -1 || selectedItemIndex === focusedIndex || textInputValue) { return; } + shouldPreventScrollAnimationRef.current = true; setFocusedIndex(selectedItemIndex); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [selectedItemIndex]);