Skip to content
34 changes: 32 additions & 2 deletions src/hooks/useSearchSelector/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import useSortedActions from '@hooks/useSortedActions';
import type {GetOptionsConfig, Option, OptionList, Options, SearchOption} from '@libs/OptionsListUtils';
import {getEmptyOptions, getSearchOptions, getSearchValueForPhoneOrEmail, getValidOptions} from '@libs/OptionsListUtils';
import {getPersonalDetailSearchTerms} from '@libs/OptionsListUtils/searchMatchUtils';
import {addSMSDomainIfPhoneNumber} from '@libs/PhoneNumber';
import type {OptionData} from '@libs/ReportUtils';
import {expensifyLoginsSelector} from '@libs/UserUtils';

Expand Down Expand Up @@ -228,10 +229,39 @@ function useSearchSelectorBase({
if (!contactOptions?.length || !areOptionsInitialized) {
return defaultOptions;
}
const personalDetailsWithContacts = defaultOptions.personalDetails.concat(contactOptions);

// Imported contacts get a generated accountID, so one sharing a login with a real Onyx user would show a duplicate
// row and be treated as a new invite. Track already-represented logins and drop contacts whose login is present.
const existingLogins = new Set<string>();

// Seed with real personal details only; optimistic ones are filtered out by getValidOptions and mustn't suppress a contact.
for (const option of defaultOptions.personalDetails) {
if (option.isOptimisticPersonalDetail) {
continue;
}
const login = addSMSDomainIfPhoneNumber(option.login ?? '').toLowerCase();
if (login) {
existingLogins.add(login);
}
}

// Keep a contact only if its login is new, deduping against both the seeded accounts and earlier contacts.
const dedupedContactOptions = contactOptions.filter((contact) => {
const login = addSMSDomainIfPhoneNumber(contact.login ?? '').toLowerCase();
if (!login || existingLogins.has(login)) {
return false;
}
existingLogins.add(login);
return true;
});

if (!dedupedContactOptions.length) {
return defaultOptions;
}

return {
...defaultOptions,
personalDetails: personalDetailsWithContacts,
personalDetails: defaultOptions.personalDetails.concat(dedupedContactOptions),
};
})();

Expand Down
315 changes: 87 additions & 228 deletions src/pages/NewChatPage/index.tsx

Large diffs are not rendered by default.

This file was deleted.

10 changes: 0 additions & 10 deletions src/pages/NewChatPage/types.ts

This file was deleted.

43 changes: 24 additions & 19 deletions src/pages/NewChatPage/useGroupChatDraftParticipantSync.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import type {LocalizedTranslate} from '@components/LocaleContextProvider';

import useIsFocusedRef from '@hooks/useIsFocusedRef';
import useOnyx from '@hooks/useOnyx';

import {getUserToInviteOption} from '@libs/OptionsListUtils';
import type {SearchOption} from '@libs/OptionsListUtils';
import {getParticipantsOption, getUserToInviteOption} from '@libs/OptionsListUtils';
import type {OptionData} from '@libs/ReportUtils';

import ONYXKEYS from '@src/ONYXKEYS';
import type {Login, PersonalDetails, PersonalDetailsList} from '@src/types/onyx';
import type {Login, PersonalDetailsList} from '@src/types/onyx';
import type NewGroupChatDraft from '@src/types/onyx/NewGroupChatDraft';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';

import type {OnyxEntry} from 'react-native-onyx';

import {useEffect, useEffectEvent, useRef} from 'react';

import type SelectedOption from './types';

/**
* Keeps the NewChatPage's `selectedOptions` state aligned with the `NEW_GROUP_CHAT_DRAFT` Onyx draft.
*
Expand All @@ -25,14 +25,14 @@ import type SelectedOption from './types';
* consistent when the user returns.
*/
function useGroupChatDraftParticipantSync(
allPersonalDetailOptions: Array<SearchOption<PersonalDetails>>,
areAllPersonalDetailOptionsLoaded: boolean,
areOptionsInitialized: boolean,
allPersonalDetails: OnyxEntry<PersonalDetailsList>,
loginList: OnyxEntry<Login>,
currentUserEmail: string,
currentUserAccountID: number,
selectedOptions: SelectedOption[],
setSelectedOptions: (options: SelectedOption[]) => void,
translate: LocalizedTranslate,
selectedOptions: OptionData[],
setSelectedOptions: (options: OptionData[]) => void,
) {
const shouldRestoreSelectedOptionsRef = useRef(true);
const isScreenFocusedRef = useIsFocusedRef();
Expand All @@ -50,18 +50,23 @@ function useGroupChatDraftParticipantSync(
// Flip the ref first so the useOnyx selector disables the subscription
shouldRestoreSelectedOptionsRef.current = false;

const restoredOptionsFromDraft = (draftParticipants ?? []).reduce<SelectedOption[]>((result, participant) => {
const restoredOptionsFromDraft = (draftParticipants ?? []).reduce<OptionData[]>((result, participant) => {
if (participant.accountID === currentUserAccountID) {
return result;
}
const option =
allPersonalDetailOptions.find((personalDetail) => personalDetail.accountID === participant.accountID) ??
getUserToInviteOption({
searchValue: participant?.login,
personalDetails: allPersonalDetails,
loginList,
currentUserEmail,
});

// Existing users resolve to their real account; participants without personal details are invited-by-email/phone
// users restored via getUserToInviteOption so they keep the optimistic flag.
const detail = allPersonalDetails?.[participant.accountID];
const option = detail
? // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- participant-shaped option built from real personal details
(getParticipantsOption({accountID: participant.accountID, login: participant.login}, allPersonalDetails, translate) as OptionData)
: getUserToInviteOption({
searchValue: participant?.login,
personalDetails: allPersonalDetails,
loginList,
currentUserEmail,
});
if (option) {
result.push({...option, isSelected: true});
}
Expand Down Expand Up @@ -94,7 +99,7 @@ function useGroupChatDraftParticipantSync(
syncSelectedOptionsWithDraft();
}, [draftParticipants, isScreenFocusedRef]);

const areRestoreInputsReady = areAllPersonalDetailOptionsLoaded && !isLoadingOnyxValue(draftParticipantsMetadata);
const areRestoreInputsReady = areOptionsInitialized && !isLoadingOnyxValue(draftParticipantsMetadata);

// Handle reload with existing draft participants
useEffect(() => {
Expand Down

This file was deleted.

Loading
Loading