Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config/eslint/eslint.seatbelt.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@
"../../src/hooks/useNewTransactions.ts" "react-hooks/refs" 2
"../../src/hooks/useOutstandingBalanceGuard.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1
"../../src/hooks/usePaymentOptions.ts" "react-hooks/refs" 1
"../../src/hooks/usePersonalDetailSearchSelector/index.native.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
"../../src/hooks/usePrevious.ts" "react-hooks/refs" 1
"../../src/hooks/useProactiveAppReview.ts" "react-hooks/purity" 1
"../../src/hooks/useRestoreInputFocus/index.android.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
Expand All @@ -234,7 +235,7 @@
"../../src/hooks/useSearchBulkActions.ts" "react-hooks/preserve-manual-memoization" 2
"../../src/hooks/useSearchBulkActions.ts" "react-hooks/refs" 1
"../../src/hooks/useSearchHighlightAndScroll.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
"../../src/hooks/useSearchSelector.native.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
"../../src/hooks/useSearchSelector/index.native.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 1
"../../src/hooks/useSelectionModeReportActions.ts" "@typescript-eslint/no-deprecated/InteractionManager.runAfterInteractions" 2
"../../src/hooks/useSidebarOrderedReports.tsx" "react-hooks/purity" 1
"../../src/hooks/useSidebarOrderedReports.tsx" "react-hooks/refs" 5
Expand Down
117 changes: 21 additions & 96 deletions src/components/Search/FilterComponents/UserSelector.tsx
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';
Expand All @@ -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>());
Comment thread
shubham1206agra marked this conversation as resolved.

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,
Comment thread
shubham1206agra marked this conversation as resolved.
includeRecentReports: false,
onSelectionChange: onChange,
});
Comment thread
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, interesting that we have recentReports in old code

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
Expand All @@ -163,7 +89,6 @@ function UserSelector({value = [], onChange}: UserSelectorProps) {
onSelectRow={selectUser}
isLoadingNewOptions={isLoadingNewOptions}
shouldShowLoadingPlaceholder={!areOptionsInitialized}
onEndReached={onListEndReached}
style={{contentContainerStyle: [styles.pb0]}}
/>
</ListFilterWrapper>
Expand Down
13 changes: 4 additions & 9 deletions src/hooks/useContactImport.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import {useCallback, useState} from 'react';
import {RESULTS} from 'react-native-permissions';
import type {PermissionStatus} from 'react-native-permissions';
import {usePersonalDetails} from '@components/OnyxListItemProvider';
import contactImport from '@libs/ContactImport';
import type {ContactImportResult} from '@libs/ContactImport/types';
import useContactPermissions from '@libs/ContactPermission/useContactPermissions';
import getContacts from '@libs/ContactUtils';
import {getContactsExtended} from '@libs/ContactUtils';
import type {SearchOption} from '@libs/OptionsListUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {PersonalDetails} from '@src/types/onyx';
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
import useLocalize from './useLocalize';
import useOnyx from './useOnyx';

Expand All @@ -32,20 +30,17 @@ type UseContactImportResult = {
function useContactImport(): UseContactImportResult {
const [contactPermissionState, setContactPermissionState] = useState<PermissionStatus>(RESULTS.UNAVAILABLE);
const [contacts, setContacts] = useState<Array<SearchOption<PersonalDetails>>>([]);
const {localeCompare} = useLocalize();
const {localeCompare, formatPhoneNumber} = useLocalize();
const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE);
const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);
const personalDetails = usePersonalDetails();
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const currentUserEmail = currentUserPersonalDetails.email ?? '';

const importAndSaveContacts = useCallback(() => {
contactImport().then(({contactList, permissionStatus}: ContactImportResult) => {
setContactPermissionState(permissionStatus);
const usersFromContact = getContacts(contactList, localeCompare, countryCode, loginList, currentUserEmail, personalDetails);
const usersFromContact = getContactsExtended(contactList, localeCompare, formatPhoneNumber, countryCode, loginList);
setContacts(usersFromContact);
});
}, [localeCompare, countryCode, loginList, currentUserEmail, personalDetails]);
}, [localeCompare, formatPhoneNumber, countryCode, loginList]);

useContactPermissions({
importAndSaveContacts,
Expand Down
59 changes: 59 additions & 0 deletions src/hooks/useContactImportNew.ts

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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);
Comment thread
shubham1206agra marked this conversation as resolved.
setContacts(usersFromContact);
});
};
Comment thread
shubham1206agra marked this conversation as resolved.

useContactPermissions({
importAndSaveContacts,
setContacts,
contactPermissionState,
setContactPermissionState,
});

return {
contacts,
contactPermissionState,
importAndSaveContacts,
setContactPermissionState,
};
}

export default useContactImport;
Loading
Loading