-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Migrate useSearchSelector.base.ts from useOptionsList to useCurrentUserPersonalDetails (part 1) #90127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrate useSearchSelector.base.ts from useOptionsList to useCurrentUserPersonalDetails (part 1) #90127
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,14 @@ | ||
| import isEmpty from 'lodash/isEmpty'; | ||
| import React, {useRef, useState} from 'react'; | ||
| import React, {useRef} from 'react'; | ||
| import {usePersonalDetails} from '@components/OnyxListItemProvider'; | ||
| import SelectionList from '@components/SelectionList'; | ||
| import UserSelectionListItem from '@components/SelectionList/ListItem/UserSelectionListItem'; | ||
| import type {ListItem, SelectionListHandle} from '@components/SelectionList/types'; | ||
| import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; | ||
| import useLocalize from '@hooks/useLocalize'; | ||
| import useOnyx from '@hooks/useOnyx'; | ||
| import useSearchSelector from '@hooks/useSearchSelector'; | ||
| import usePersonalDetailSearchSelector from '@hooks/usePersonalDetailSearchSelector'; | ||
| import useThemeStyles from '@hooks/useThemeStyles'; | ||
| import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; | ||
| import {getParticipantsOption} from '@libs/OptionsListUtils'; | ||
| import {doesPersonalDetailMatchSearchTerm} from '@libs/OptionsListUtils/searchMatchUtils'; | ||
| import type {OptionData} from '@libs/ReportUtils'; | ||
| import type {OptionData} from '@libs/PersonalDetailOptionsListUtils'; | ||
| import CONST from '@src/CONST'; | ||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||
| import ListFilterWrapper from './ListFilterViewWrapper'; | ||
|
|
@@ -27,116 +23,46 @@ function UserSelector({value = [], onChange}: UserSelectorProps) { | |
| const styles = useThemeStyles(); | ||
| const {translate} = useLocalize(); | ||
| const personalDetails = usePersonalDetails(); | ||
| const currentUserPersonalDetails = useCurrentUserPersonalDetails(); | ||
| const currentUserAccountID = currentUserPersonalDetails.accountID; | ||
| const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); | ||
| const [isSearchingForReports] = useOnyx(ONYXKEYS.RAM_ONLY_IS_SEARCHING_FOR_REPORTS); | ||
| const initialSelectedOptions = value.reduce<OptionData[]>((options, id) => { | ||
| const initialSelectedAccountIDs = value.reduce<Set<string>>((acc, id) => { | ||
| const participant = personalDetails?.[id]; | ||
| if (!participant) { | ||
| return options; | ||
| return acc; | ||
| } | ||
|
|
||
| const optionData = { | ||
| ...getParticipantsOption(participant, personalDetails), | ||
| isSelected: true, | ||
| }; | ||
| acc.add(id); | ||
| return acc; | ||
| }, new Set<string>()); | ||
|
|
||
| if (optionData) { | ||
| options.push(optionData as OptionData); | ||
| } | ||
|
|
||
| return options; | ||
| }, []); | ||
|
|
||
| const {searchTerm, debouncedSearchTerm, setSearchTerm, availableOptions, toggleSelection, areOptionsInitialized, selectedOptionsForDisplay, onListEndReached} = useSearchSelector({ | ||
| const {searchTerm, setSearchTerm, availableOptions, totalOptionsCount, toggleSelection, areOptionsInitialized} = usePersonalDetailSearchSelector({ | ||
| selectionMode: CONST.SEARCH_SELECTOR.SELECTION_MODE_MULTI, | ||
| searchContext: CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_GENERAL, | ||
| initialSelected: initialSelectedOptions, | ||
| initialSelected: initialSelectedAccountIDs, | ||
| excludeLogins: CONST.EXPENSIFY_EMAILS_OBJECT, | ||
| maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, | ||
| includeUserToInvite: false, | ||
| includeCurrentUser: true, | ||
| onSelectionChange: (options) => onChange(options.flatMap((option) => (option.accountID ? [option.accountID.toString()] : []))), | ||
| includeCurrentUser: false, | ||
|
shubham1206agra marked this conversation as resolved.
|
||
| includeRecentReports: false, | ||
| onSelectionChange: onChange, | ||
| }); | ||
|
shubham1206agra marked this conversation as resolved.
|
||
|
|
||
| const listData = (() => { | ||
| const personalDetailsList = availableOptions.personalDetails.map((participant) => ({ | ||
| ...participant, | ||
| keyForList: String(participant.accountID), | ||
| })); | ||
| const recentReports = availableOptions.recentReports.map((report) => ({ | ||
| ...report, | ||
| keyForList: String(report.reportID), | ||
| })); | ||
|
|
||
| const isCurrentUserSelected = selectedOptionsForDisplay.some((option) => option.accountID === currentUserAccountID); | ||
|
|
||
| // Extract the current user from available options to guarantee they appear at the top. | ||
| // Falls back to creating from personal details to handle pagination edge cases. | ||
| let currentUserOption: OptionData | undefined; | ||
| if (!isCurrentUserSelected && currentUserAccountID) { | ||
| const currentUserPersonalDetail = personalDetailsList.find((p) => p.accountID === currentUserAccountID) ?? recentReports.find((r) => r.accountID === currentUserAccountID); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, interesting that we have |
||
| if (currentUserPersonalDetail) { | ||
| currentUserOption = currentUserPersonalDetail; | ||
| } else if (personalDetails?.[currentUserAccountID]) { | ||
| const candidateOption = getParticipantsOption(personalDetails[currentUserAccountID], personalDetails) as OptionData; | ||
| const trimmedSearchTerm = debouncedSearchTerm.trim().toLowerCase(); | ||
| if (!trimmedSearchTerm || doesPersonalDetailMatchSearchTerm(candidateOption, currentUserAccountID, trimmedSearchTerm)) { | ||
| currentUserOption = candidateOption; | ||
| } | ||
| } | ||
| if (!availableOptions.currentUserOption) { | ||
| return [...availableOptions.selectedOptions, ...availableOptions.personalDetails]; | ||
| } | ||
|
|
||
| // Filter current user from regular lists to avoid duplication | ||
| const filteredPersonalDetails = currentUserOption ? personalDetailsList.filter((p) => p.accountID !== currentUserAccountID) : personalDetailsList; | ||
| const filteredRecentReports = currentUserOption ? recentReports.filter((r) => r.accountID !== currentUserAccountID) : recentReports; | ||
|
|
||
| // Place selected options first, then the current user, then the rest | ||
| const combinedOptions = [...selectedOptionsForDisplay, ...(currentUserOption ? [currentUserOption] : []), ...filteredPersonalDetails, ...filteredRecentReports]; | ||
|
|
||
| // Sort so that selected items appear first; current user placement is handled explicitly above | ||
| combinedOptions.sort((a, b) => { | ||
| if (a.isSelected && !b.isSelected) { | ||
| return -1; | ||
| } | ||
| if (!a.isSelected && b.isSelected) { | ||
| return 1; | ||
| } | ||
| // Among selected items, prioritize the current user | ||
| if (a.isSelected && b.isSelected) { | ||
| if (a.accountID === currentUserAccountID) { | ||
| return -1; | ||
| } | ||
| if (b.accountID === currentUserAccountID) { | ||
| return 1; | ||
| } | ||
| } | ||
| return 0; | ||
| }); | ||
|
|
||
| const combinedOptionsWithKeyForList = combinedOptions.map((option) => ({ | ||
| ...option, | ||
| keyForList: option.keyForList ?? option.login ?? '', | ||
| })); | ||
| return combinedOptionsWithKeyForList; | ||
| const isCurrentOptionSelected = availableOptions.currentUserOption.isSelected; | ||
| if (isCurrentOptionSelected) { | ||
| return [availableOptions.currentUserOption, ...availableOptions.selectedOptions, ...availableOptions.personalDetails]; | ||
| } | ||
| return [...availableOptions.selectedOptions, availableOptions.currentUserOption, ...availableOptions.personalDetails]; | ||
| })(); | ||
|
|
||
| const headerMessage = isEmpty(listData) ? translate('common.noResultsFound') : undefined; | ||
| const headerMessage = listData.length === 0 ? translate('common.noResultsFound') : undefined; | ||
|
|
||
| const selectUser = (option: OptionData) => { | ||
| toggleSelection(option); | ||
| selectionListRef?.current?.scrollToIndex(0); | ||
| }; | ||
|
|
||
| const isLoadingNewOptions = !!isSearchingForReports; | ||
| const totalOptions = selectedOptionsForDisplay.length + availableOptions.personalDetails.length + availableOptions.recentReports.length; | ||
| const [totalOptionsCount, setTotalOptionsCount] = useState(totalOptions); | ||
|
|
||
| if (totalOptions !== totalOptionsCount && !debouncedSearchTerm) { | ||
| setTotalOptionsCount(selectedOptionsForDisplay.length + availableOptions.personalDetails.length + availableOptions.recentReports.length); | ||
| } | ||
|
|
||
| const shouldShowSearchInput = totalOptionsCount >= CONST.STANDARD_LIST_ITEM_LIMIT; | ||
|
|
||
| const textInputOptions = shouldShowSearchInput | ||
|
|
@@ -163,7 +89,6 @@ function UserSelector({value = [], onChange}: UserSelectorProps) { | |
| onSelectRow={selectUser} | ||
| isLoadingNewOptions={isLoadingNewOptions} | ||
| shouldShowLoadingPlaceholder={!areOptionsInitialized} | ||
| onEndReached={onListEndReached} | ||
| style={{contentContainerStyle: [styles.pb0]}} | ||
| /> | ||
| </ListFilterWrapper> | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One note for everyone. We will gradually switch the use of useContactImport to the newer implementation in subsequent PRs. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import {useState} from 'react'; | ||
| import {RESULTS} from 'react-native-permissions'; | ||
| import type {PermissionStatus} from 'react-native-permissions'; | ||
| import contactImport from '@libs/ContactImport'; | ||
| import type {ContactImportResult} from '@libs/ContactImport/types'; | ||
| import useContactPermissions from '@libs/ContactPermission/useContactPermissions'; | ||
| import {getContacts} from '@libs/ContactUtils'; | ||
| import type {OptionData} from '@libs/PersonalDetailOptionsListUtils'; | ||
| import CONST from '@src/CONST'; | ||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||
| import useLocalize from './useLocalize'; | ||
| import useOnyx from './useOnyx'; | ||
|
|
||
| /** | ||
| * Return type of the useContactImport hook. | ||
| */ | ||
| type UseContactImportResult = { | ||
| contacts: OptionData[]; | ||
| contactPermissionState: PermissionStatus; | ||
| importAndSaveContacts: () => void; | ||
| setContactPermissionState: React.Dispatch<React.SetStateAction<PermissionStatus>>; | ||
| }; | ||
|
|
||
| /** | ||
| * Custom hook that handles importing device contacts, | ||
| * managing permissions, and transforming contact data | ||
| * into a format suitable for use in the app. | ||
| */ | ||
| function useContactImport(): UseContactImportResult { | ||
| const [contactPermissionState, setContactPermissionState] = useState<PermissionStatus>(RESULTS.UNAVAILABLE); | ||
| const [contacts, setContacts] = useState<OptionData[]>([]); | ||
| const {localeCompare, formatPhoneNumber} = useLocalize(); | ||
| const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE); | ||
| const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); | ||
|
|
||
| const importAndSaveContacts = () => { | ||
| contactImport().then(({contactList, permissionStatus}: ContactImportResult) => { | ||
| setContactPermissionState(permissionStatus); | ||
| const usersFromContact = getContacts(contactList, localeCompare, formatPhoneNumber, countryCode, loginList); | ||
|
shubham1206agra marked this conversation as resolved.
|
||
| setContacts(usersFromContact); | ||
| }); | ||
| }; | ||
|
shubham1206agra marked this conversation as resolved.
|
||
|
|
||
| useContactPermissions({ | ||
| importAndSaveContacts, | ||
| setContacts, | ||
| contactPermissionState, | ||
| setContactPermissionState, | ||
| }); | ||
|
|
||
| return { | ||
| contacts, | ||
| contactPermissionState, | ||
| importAndSaveContacts, | ||
| setContactPermissionState, | ||
| }; | ||
| } | ||
|
|
||
| export default useContactImport; | ||
Uh oh!
There was an error while loading. Please reload this page.