diff --git a/lib/shared/mention-utils.js b/lib/shared/mention-utils.js --- a/lib/shared/mention-utils.js +++ b/lib/shared/mention-utils.js @@ -8,10 +8,11 @@ } from './thread-utils.js'; import { stringForUserExplicit } from './user-utils.js'; import { threadTypes } from '../types/thread-types-enum.js'; -import { - type ThreadInfo, - type RelativeMemberInfo, - type ResolvedThreadInfo, +import type { + ThreadInfo, + RelativeMemberInfo, + ResolvedThreadInfo, + ChatMentionCandidates, } from '../types/thread-types.js'; export type TypeaheadMatchedStrings = { @@ -31,7 +32,7 @@ type MentionTypeaheadChatSuggestionItem = { type: 'chat', - chat: ResolvedThreadInfo, + threadInfo: ResolvedThreadInfo, }; export type MentionTypeaheadSuggestionItem = @@ -106,6 +107,31 @@ .map(userInfo => ({ type: 'user', userInfo })); } +function getMentionTypeaheadChatSuggestions( + chatSearchIndex: SentencePrefixSearchIndex, + chatMentionCandidates: ChatMentionCandidates, + chatPrefix: string, +): $ReadOnlyArray { + if (chatPrefix.length === 0) { + return Object.keys(chatMentionCandidates).map(threadID => ({ + type: 'chat', + threadInfo: chatMentionCandidates[threadID], + })); + } + const threadIDs = chatSearchIndex.getSearchResults(chatPrefix); + const result = []; + for (const threadID of threadIDs) { + if (!chatMentionCandidates[threadID]) { + continue; + } + result.push({ + type: 'chat', + threadInfo: chatMentionCandidates[threadID], + }); + } + return result; +} + function getNewTextAndSelection( textBeforeAtSymbol: string, entireText: string, @@ -148,6 +174,7 @@ isUserMentioned, extractUserMentionsFromText, getMentionTypeaheadUserSuggestions, + getMentionTypeaheadChatSuggestions, getNewTextAndSelection, getTypeaheadRegexMatches, getUserMentionsCandidates, diff --git a/native/avatars/thread-avatar.react.js b/native/avatars/thread-avatar.react.js --- a/native/avatars/thread-avatar.react.js +++ b/native/avatars/thread-avatar.react.js @@ -8,13 +8,17 @@ } from 'lib/shared/avatar-utils.js'; import { getSingleOtherUser } from 'lib/shared/thread-utils.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; -import { type RawThreadInfo, type ThreadInfo } from 'lib/types/thread-types.js'; +import type { + RawThreadInfo, + ThreadInfo, + ResolvedThreadInfo, +} from 'lib/types/thread-types.js'; import Avatar, { type AvatarSize } from './avatar.react.js'; import { useSelector } from '../redux/redux-utils.js'; type Props = { - +threadInfo: RawThreadInfo | ThreadInfo, + +threadInfo: RawThreadInfo | ThreadInfo | ResolvedThreadInfo, +size: AvatarSize, }; diff --git a/native/chat/chat-input-bar.react.js b/native/chat/chat-input-bar.react.js --- a/native/chat/chat-input-bar.react.js +++ b/native/chat/chat-input-bar.react.js @@ -38,6 +38,7 @@ import { useEditMessage } from 'lib/shared/edit-messages-utils.js'; import { getMentionTypeaheadUserSuggestions, + getMentionTypeaheadChatSuggestions, getTypeaheadRegexMatches, type Selection, getUserMentionsCandidates, @@ -58,6 +59,7 @@ checkIfDefaultMembersAreVoiced, draftKeyFromThreadID, useThreadChatMentionCandidates, + useThreadChatMentionSearchIndex, } from 'lib/shared/thread-utils.js'; import type { CalendarQuery } from 'lib/types/entry-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; @@ -74,6 +76,7 @@ ClientThreadJoinRequest, ThreadJoinPayload, RelativeMemberInfo, + ChatMentionCandidates, } from 'lib/types/thread-types.js'; import { type UserInfos } from 'lib/types/user-types.js'; import { @@ -173,6 +176,8 @@ +inputState: ?InputState, +userSearchIndex: SentencePrefixSearchIndex, +userMentionsCandidates: $ReadOnlyArray, + +chatMentionSearchIndex: SentencePrefixSearchIndex, + +chatMentionCandidates: ChatMentionCandidates, +parentThreadInfo: ?ThreadInfo, +editedMessagePreview: ?MessagePreviewResult, +editedMessageInfo: ?MessageInfo, @@ -565,13 +570,19 @@ this.props.viewerID, typeaheadMatchedStrings.textPrefix, ); + const suggestedChats = getMentionTypeaheadChatSuggestions( + this.props.chatMentionSearchIndex, + this.props.chatMentionCandidates, + typeaheadMatchedStrings.textPrefix, + ); + const suggestions = [...suggestedUsers, ...suggestedChats]; - if (suggestedUsers.length > 0) { + if (suggestions.length > 0) { typeaheadTooltip = ( parentThreadID ? threadInfoSelector(state)[parentThreadID] : null, @@ -1299,6 +1314,8 @@ inputState={inputState} userSearchIndex={userSearchIndex} userMentionsCandidates={userMentionsCandidates} + chatMentionSearchIndex={chatMentionSearchIndex} + chatMentionCandidates={chatMentionCandidates} parentThreadInfo={parentThreadInfo} editedMessagePreview={editedMessagePreview} editedMessageInfo={editedMessageInfo} diff --git a/native/utils/typeahead-utils.js b/native/utils/typeahead-utils.js --- a/native/utils/typeahead-utils.js +++ b/native/utils/typeahead-utils.js @@ -11,15 +11,17 @@ type TypeaheadTooltipActionItem, type MentionTypeaheadSuggestionItem, } from 'lib/shared/mention-utils.js'; +import { validChatNameRegexString } from 'lib/shared/thread-utils.js'; import { stringForUserExplicit } from 'lib/shared/user-utils.js'; +import ThreadAvatar from '../avatars/thread-avatar.react.js'; import UserAvatar from '../avatars/user-avatar.react.js'; import type { TypeaheadTooltipStyles } from '../chat/typeahead-tooltip.react.js'; // Native regex is a little bit different than web one as // there are no named capturing groups yet on native. const nativeMentionTypeaheadRegex: RegExp = new RegExp( - `((^(.|\n)*\\s+)|^)@(${oldValidUsernameRegexString})?$`, + `((^(.|\n)*\\s+)|^)@(${oldValidUsernameRegexString}|${validChatNameRegexString})?$`, ); export type TypeaheadTooltipActionsParams = { @@ -67,12 +69,12 @@ }, }); } else if (suggestion.type === 'chat') { - const { chat } = suggestion; - const mentionText = `@[[${chat.id}:${encodeChatMentionText( - chat.uiName, + const { threadInfo } = suggestion; + const mentionText = `@[[${threadInfo.id}:${encodeChatMentionText( + threadInfo.uiName, )}]]`; actions.push({ - key: chat.id, + key: threadInfo.id, execute: () => { const { newText, newSelectionStart } = getNewTextAndSelection( textBeforeAtSymbol, @@ -87,7 +89,7 @@ }, actionButtonContent: { type: 'chat', - chat, + threadInfo, }, }); } @@ -110,6 +112,14 @@ ); typeaheadTooltipButtonText = item.actionButtonContent.userInfo.username; + } else if (item.actionButtonContent.type === 'chat') { + typeaheadTooltipButtonText = item.actionButtonContent.threadInfo.uiName; + avatarComponent = ( + + ); } return ( <>