Skip to content
Open
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
12 changes: 7 additions & 5 deletions src/hooks/usePersonalDetailSearchSelector/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type UseSearchSelectorConfig = {
/** Logins to exclude from suggestions only (soft exclusions - can still be manually entered) */
excludeFromSuggestionsOnly?: Record<string, boolean>;

/** Whether to include recent reports (for getMemberInviteOptions) */
/** Whether to include recent reports */
includeRecentReports?: boolean;

/** Whether to include current user */
Expand All @@ -48,8 +48,8 @@ type UseSearchSelectorConfig = {
/** Enable phone contacts integration */
enablePhoneContacts?: boolean;

/** Callback when selection changes (multi-select mode) */
onSelectionChange?: (selected: string[]) => void;
/** Callback when selection changes (multi-select mode). Receives the new selected accountIDs and the new selected options. */
onSelectionChange?: (selected: string[], selectedOptions: OptionData[]) => void;

/** Callback when single option is selected (single-select mode) */
onSingleSelect?: (option: OptionData) => void;
Expand Down Expand Up @@ -280,11 +280,13 @@ function usePersonalDetailSearchSelectorBase({
}
const newSet = new Set([...selectedAccountIDs].filter((accountID) => accountID !== option.accountID.toString()));
setSelectedAccountIDs(newSet);
onSelectionChange?.(Array.from(newSet));
const newSelectedOptions = selectedOptions.filter((selected) => selected.accountID !== option.accountID);
onSelectionChange?.(Array.from(newSet), newSelectedOptions);
} else {
const newSet = new Set(selectedAccountIDs).add(option.accountID.toString());
setSelectedAccountIDs(newSet);
onSelectionChange?.(Array.from(newSet));
const newSelectedOptions = [...selectedOptions, {...option, isSelected: true}];
onSelectionChange?.(Array.from(newSet), newSelectedOptions);
if (!existingAccountIDs.has(option.accountID.toString())) {
setExtraOptions((prev) => [...prev, {...option, isSelected: true}]);
}
Expand Down
51 changes: 2 additions & 49 deletions src/libs/OptionsListUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import Navigation from '@libs/Navigation/Navigation';
import {getIsOffline} from '@libs/NetworkState';
import Parser from '@libs/Parser';
import type {OptionData as PersonalDetailOptionData} from '@libs/PersonalDetailOptionsListUtils/types';
import {getLoginByAccountID, getPersonalDetailByEmail, getPersonalDetailsListByIDs, temporaryGetDisplayNameOrDefault} from '@libs/PersonalDetailsUtils';
import {getLoginByAccountID, getPersonalDetailsListByIDs, temporaryGetDisplayNameOrDefault} from '@libs/PersonalDetailsUtils';
import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from '@libs/PhoneNumber';
import {
canSendInvoiceFromWorkspace,
Expand Down Expand Up @@ -187,7 +187,7 @@ import type {
Transaction,
VisibleReportActionsDerivedValue,
} from '@src/types/onyx';
import type {Attendee, Participant} from '@src/types/onyx/IOU';
import type {Participant} from '@src/types/onyx/IOU';
import {isEmptyObject} from '@src/types/utils/EmptyObject';

import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
Expand All @@ -206,7 +206,6 @@ import type {
GetValidReportsConfig,
IsValidReportsConfig,
MemberForList,
Option,
OptionList,
Options,
OptionsResult,
Expand Down Expand Up @@ -2825,51 +2824,6 @@ function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: OnyxEn
};
}

function getFilteredRecentAttendees(
personalDetails: OnyxEntry<PersonalDetailsList>,
attendees: Attendee[],
recentAttendees: Attendee[],
currentUserEmail: string,
currentUserAccountID: number,
translate: LocalizedTranslate,
): Option[] {
const recentAttendeeHasCurrentUser = recentAttendees.find((attendee) => attendee.email === currentUserEmail);
if (!recentAttendeeHasCurrentUser && currentUserEmail) {
const details = getPersonalDetailByEmail(currentUserEmail);
recentAttendees.push({
email: currentUserEmail,
displayName: details?.displayName ?? currentUserEmail,
avatarUrl: details?.avatarThumbnail ?? '',
});
}

// Deduplicate recentAttendees: use email for regular users, displayName for name-only attendees
const seenAttendees = new Set<string>();
const deduplicatedRecentAttendees = recentAttendees.filter((attendee) => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const key = attendee.email || attendee.displayName || '';
if (seenAttendees.has(key)) {
return false;
}
seenAttendees.add(key);
return true;
});

const filteredRecentAttendees = deduplicatedRecentAttendees
.filter((attendee) => !attendees.find(({email, displayName}) => (attendee.email ? email === attendee.email : displayName === attendee.displayName)))
.map((attendee) => ({
...attendee,
// Use || instead of ?? to handle empty string email for name-only attendees
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
login: attendee.email || attendee.displayName,
...getPersonalDetailByEmail(attendee.email),
keyForList: `${currentUserAccountID}`,
}))
.map((attendee) => getParticipantsOption(attendee, personalDetails, translate));

return filteredRecentAttendees;
}

/**
* Format personalDetails or userToInvite to be shown in the list
*
Expand Down Expand Up @@ -3363,7 +3317,6 @@ export {
formatMemberForList,
formatSectionsFromSearchTerm,
getAlternateText,
getFilteredRecentAttendees,
getEmptyOptions,
getHeaderMessage,
getHeaderMessageForNonUserList,
Expand Down
34 changes: 34 additions & 0 deletions src/libs/PersonalDetailOptionsListUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {LoginList, OnyxInputOrEntry, PersonalDetails, PersonalDetailsList, Report, ReportAttributesDerivedValue} from '@src/types/onyx';
import type {ReportAttributes} from '@src/types/onyx/DerivedValues';
import type {Attendee} from '@src/types/onyx/IOU';
import {isEmptyObject} from '@src/types/utils/EmptyObject';

import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
Expand Down Expand Up @@ -365,6 +366,7 @@ function getValidOptions(
}
}
}
recentOptions = optionsOrderBy(recentOptions, personalDetailsComparator, recentMaxElements, undefined, true).options;
Comment thread
shubham1206agra marked this conversation as resolved.
} else if (includeRecentReports) {
// if maxElements is passed, filter the recent reports by searchString and return only most recent reports (@see recentReportsComparator)
const filteringFunction = (option: OptionData) => {
Expand Down Expand Up @@ -499,6 +501,37 @@ function getHeaderMessage(translate: LocaleContextProps['translate'], searchValu
return translate('common.noResultsFound');
}

/**
* Returns the logins of recent attendees to suggest, ensuring the current user is included, deduplicated,
* and excluding attendees that are already selected. The returned logins are fed to getValidOptions as
* `recentAttendees`, which rebuilds the full options (matching personal details or creating optimistic ones).
*/
function getFilteredRecentAttendees(attendees: Attendee[], recentAttendees: Attendee[], currentUserEmail: string): string[] {
const allRecentAttendees = [...recentAttendees];
if (currentUserEmail && !allRecentAttendees.some((attendee) => attendee.email === currentUserEmail)) {
allRecentAttendees.push({email: currentUserEmail, displayName: currentUserEmail, avatarUrl: ''});
}

const seenAttendees = new Set<string>();
return (
allRecentAttendees
.filter((attendee) => {
// Deduplicate: use email for regular users, displayName for name-only attendees
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const key = attendee.email || attendee.displayName || '';
if (!key || seenAttendees.has(key)) {
return false;
}
seenAttendees.add(key);
return true;
})
.filter((attendee) => !attendees.find(({email, displayName}) => (attendee.email ? email === attendee.email : displayName === attendee.displayName)))
// Use || to fall back to displayName for name-only attendees (empty email)
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
.map((attendee) => attendee.email || attendee.displayName)
);
}

export {
createOption,
getContactOption,
Expand All @@ -509,6 +542,7 @@ export {
createOptionList,
getHeaderMessage,
getUserToInviteOption,
getFilteredRecentAttendees,
};

export type {OptionData};
Loading
Loading