diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 431cd1a3e820..3bebd5a0813a 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -165,6 +165,16 @@ type GetOptionsConfig = { transactionViolations?: OnyxCollection; }; +type GetUserToInviteConfig = { + searchValue: string; + excludeUnknownUsers?: boolean; + optionsToExclude?: Array>; + selectedOptions?: Array>; + betas: OnyxEntry; + reportActions?: ReportActions; + showChatPreviewLine?: boolean; +}; + type MemberForList = { text: string; alternateText: string; @@ -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; + + 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 */ @@ -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 !== '') { @@ -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 = []): Options { const searchValue = getSearchValueForPhoneOrEmail(searchInputValue); const searchTerms = searchValue ? searchValue.split(' ') : []; @@ -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: [], diff --git a/src/pages/ChatFinderPage/index.tsx b/src/pages/ChatFinderPage/index.tsx index a7516163c40b..9a15fce6718a 100644 --- a/src/pages/ChatFinderPage/index.tsx +++ b/src/pages/ChatFinderPage/index.tsx @@ -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;