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
142 changes: 99 additions & 43 deletions src/libs/OptionsListUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ type GetOptionsConfig = {
transactionViolations?: OnyxCollection<TransactionViolation[]>;
};

type GetUserToInviteConfig = {
searchValue: string;
excludeUnknownUsers?: boolean;
optionsToExclude?: Array<Partial<ReportUtils.OptionData>>;
selectedOptions?: Array<Partial<ReportUtils.OptionData>>;
betas: OnyxEntry<Beta[]>;
reportActions?: ReportActions;
showChatPreviewLine?: boolean;
};

type MemberForList = {
text: string;
alternateText: string;
Expand Down Expand Up @@ -1536,6 +1546,74 @@ function orderOptions(options: ReportUtils.OptionData[], searchValue: string | u
);
}

/**
* We create a new user option if the following conditions are satisfied:
* - There's no matching recent report and personal detail option
* - The searchValue is a valid email or phone number
* - The searchValue isn't the current personal detail login
* - We can use chronos or the search value is not the chronos email
*/
function getUserToInviteOption({
searchValue,
excludeUnknownUsers = false,
optionsToExclude = [],
selectedOptions = [],
betas,
reportActions = {},
showChatPreviewLine = false,
}: GetUserToInviteConfig): ReportUtils.OptionData | null {
const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchValue)));
const isCurrentUserLogin = isCurrentUser({login: searchValue} as PersonalDetails);
const isInSelectedOption = selectedOptions.some((option) => 'login' in option && option.login === searchValue);
const isValidEmail = Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN);
const isValidPhoneNumber = parsedPhoneNumber.possible && Str.isValidE164Phone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? ''));
const isInOptionToExclude =
optionsToExclude.findIndex((optionToExclude) => 'login' in optionToExclude && optionToExclude.login === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) !== -1;
const isChronosEmail = searchValue === CONST.EMAIL.CHRONOS;

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.

Thanks looks soooooo much better ❤️


if (
!searchValue ||
isCurrentUserLogin ||
isInSelectedOption ||
(!isValidEmail && !isValidPhoneNumber) ||
isInOptionToExclude ||
(isChronosEmail && !Permissions.canUseChronos(betas)) ||
excludeUnknownUsers
) {
return null;
}

// Generates an optimistic account ID for new users not yet saved in Onyx
const optimisticAccountID = UserUtils.generateAccountID(searchValue);
const personalDetailsExtended = {
...allPersonalDetails,
[optimisticAccountID]: {
accountID: optimisticAccountID,
login: searchValue,
},
};
const userToInvite = createOption([optimisticAccountID], personalDetailsExtended, null, reportActions, {
showChatPreviewLine,
});
userToInvite.isOptimisticAccount = true;
userToInvite.login = searchValue;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
userToInvite.text = userToInvite.text || searchValue;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
userToInvite.alternateText = userToInvite.alternateText || searchValue;

// If user doesn't exist, use a fallback avatar
userToInvite.icons = [
{
source: UserUtils.getAvatar('', optimisticAccountID),
name: searchValue,
type: CONST.ICON_TYPE_AVATAR,
},
];

return userToInvite;
}

/**
* filter options based on specific conditions
*/
Expand Down Expand Up @@ -1844,52 +1922,23 @@ function getOptions(
currentUserOption = undefined;
}

let userToInvite: ReportUtils.OptionData | null = null;
const noOptions = recentReportOptions.length + personalDetailsOptions.length === 0 && !currentUserOption;
const noOptionsMatchExactly = !personalDetailsOptions
.concat(recentReportOptions)
.find((option) => option.login === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue ?? '').toLowerCase() || option.login === searchValue?.toLowerCase());

if (
searchValue &&
(noOptions || noOptionsMatchExactly) &&
!isCurrentUser({login: searchValue} as PersonalDetails) &&
selectedOptions.every((option) => 'login' in option && option.login !== searchValue) &&
((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN)) ||
(parsedPhoneNumber.possible && Str.isValidE164Phone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) &&
!optionsToExclude.find((optionToExclude) => 'login' in optionToExclude && optionToExclude.login === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) &&
(searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) &&
!excludeUnknownUsers
) {
// Generates an optimistic account ID for new users not yet saved in Onyx
const optimisticAccountID = UserUtils.generateAccountID(searchValue);
const personalDetailsExtended = {
...allPersonalDetails,
[optimisticAccountID]: {
accountID: optimisticAccountID,
login: searchValue,
avatar: UserUtils.getDefaultAvatar(optimisticAccountID),
},
};
userToInvite = createOption([optimisticAccountID], personalDetailsExtended, null, reportActions, {
showChatPreviewLine,
});
userToInvite.isOptimisticAccount = true;
userToInvite.login = searchValue;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
userToInvite.text = userToInvite.text || searchValue;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
userToInvite.alternateText = userToInvite.alternateText || searchValue;

// If user doesn't exist, use a default avatar
userToInvite.icons = [
{
source: UserUtils.getAvatar('', optimisticAccountID),
name: searchValue,
type: CONST.ICON_TYPE_AVATAR,
},
];
}
const userToInvite =
noOptions || noOptionsMatchExactly
? getUserToInviteOption({
searchValue,
excludeUnknownUsers,
optionsToExclude,
selectedOptions,
betas,
reportActions,
showChatPreviewLine,
})
: null;

// If we are prioritizing 1:1 chats in search, do it only once we started searching
if (sortByReportTypeInSearch && searchValue !== '') {
Expand Down Expand Up @@ -2249,7 +2298,7 @@ function getFirstKeyForList(data?: Option[] | null) {
/**
* Filters options based on the search input value
*/
function filterOptions(options: Options, searchInputValue: string): Options {
function filterOptions(options: Options, searchInputValue: string, betas: OnyxEntry<Beta[]> = []): Options {
const searchValue = getSearchValueForPhoneOrEmail(searchInputValue);
const searchTerms = searchValue ? searchValue.split(' ') : [];

Expand Down Expand Up @@ -2318,11 +2367,18 @@ function filterOptions(options: Options, searchInputValue: string): Options {
}, options);

const recentReports = matchResults.recentReports.concat(matchResults.personalDetails);
const userToInvite =
recentReports.length === 0
? getUserToInviteOption({
searchValue,
betas,
})
: null;

return {
personalDetails: [],
recentReports: orderOptions(recentReports, searchValue),
userToInvite: null,
userToInvite,
currentUserOption: null,
categoryOptions: [],
tagOptions: [],
Expand Down
8 changes: 4 additions & 4 deletions src/pages/ChatFinderPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,15 @@ function ChatFinderPage({betas, isSearchingForReports, navigation}: ChatFinderPa
};
}

const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedSearchValue);
const header = OptionsListUtils.getHeaderMessage(newOptions.recentReports.length > 0, false, debouncedSearchValue);
const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedSearchValue, betas);
const header = OptionsListUtils.getHeaderMessage(newOptions.recentReports.length + Number(!!newOptions.userToInvite) > 0, false, debouncedSearchValue);
return {
recentReports: newOptions.recentReports,
personalDetails: newOptions.personalDetails,
userToInvite: null,
userToInvite: newOptions.userToInvite,
headerMessage: header,
};
}, [debouncedSearchValue, searchOptions]);
}, [debouncedSearchValue, searchOptions, betas]);

const {recentReports, personalDetails: localPersonalDetails, userToInvite, headerMessage} = debouncedSearchValue.trim() !== '' ? filteredOptions : searchOptions;

Expand Down