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,9 +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, +import type { + ThreadInfo, + RelativeMemberInfo, + ResolvedThreadInfo, + ChatMentionCandidates, } from '../types/thread-types.js'; export type TypeaheadMatchedStrings = { @@ -84,6 +86,21 @@ ); } +function getMentionTypeaheadChatSuggestions( + chatSearchIndex: SentencePrefixSearchIndex, + chatMentionCandidates: ChatMentionCandidates, + chatPrefix: string, +): $ReadOnlyArray { + const threadIDs = chatSearchIndex.getSearchResults(chatPrefix); + const result = []; + for (const threadID in chatMentionCandidates) { + if (chatPrefix.length === 0 || threadIDs.includes(threadID)) { + result.push(chatMentionCandidates[threadID]); + } + } + return result; +} + function getNewTextAndSelection( textBeforeAtSymbol: string, entireText: string, @@ -126,6 +143,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 from './avatar.react.js'; import { useSelector } from '../redux/redux-utils.js'; type Props = { - +threadInfo: RawThreadInfo | ThreadInfo, + +threadInfo: RawThreadInfo | ThreadInfo | ResolvedThreadInfo, +size: 'micro' | 'small' | 'large' | 'profile', }; 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, @@ -59,6 +60,7 @@ checkIfDefaultMembersAreVoiced, draftKeyFromThreadID, useThreadChatMentionCandidates, + useThreadChatMentionSearchIndex, } from 'lib/shared/thread-utils.js'; import { stringForUserExplicit } from 'lib/shared/user-utils.js'; import type { CalendarQuery } from 'lib/types/entry-types.js'; @@ -77,6 +79,7 @@ ThreadJoinPayload, RelativeMemberInfo, ResolvedThreadInfo, + ChatMentionCandidates, } from 'lib/types/thread-types.js'; import { type UserInfos } from 'lib/types/user-types.js'; import { @@ -93,6 +96,7 @@ } from './message-editing-context.react.js'; import type { RemoveEditMode } from './message-list-types.js'; import TypeaheadTooltip from './typeahead-tooltip.react.js'; +import ThreadAvatar from '../avatars/thread-avatar.react.js'; import UserAvatar from '../avatars/user-avatar.react.js'; import Button from '../components/button.react.js'; // eslint-disable-next-line import/extensions @@ -173,6 +177,8 @@ +inputState: ?InputState, +userSearchIndex: SentencePrefixSearchIndex, +userMentionsCandidates: $ReadOnlyArray, + +chatMentionSearchIndex: SentencePrefixSearchIndex, + +chatMentionCandidates: ChatMentionCandidates, +parentThreadInfo: ?ThreadInfo, +editedMessagePreview: ?MessagePreviewResult, +editedMessageInfo: ?MessageInfo, @@ -505,6 +511,9 @@ if (item.type === 'user') { text = suggestionText; avatarComponent = ; + } else if (item.type === 'chat') { + text = `@${item.chat.uiName}`; + avatarComponent = ; } return ( <> @@ -596,10 +605,15 @@ this.props.viewerID, typeaheadMatchedStrings.textPrefix, ); - const suggestions = suggestedUsers.map(user => ({ - type: 'user', - user, - })); + const suggestedChats = getMentionTypeaheadChatSuggestions( + this.props.chatMentionSearchIndex, + this.props.chatMentionCandidates, + typeaheadMatchedStrings.textPrefix, + ); + const suggestions = [ + ...suggestedUsers.map(user => ({ type: 'user', user })), + ...suggestedChats.map(chat => ({ type: 'chat', chat })), + ]; if (suggestions.length > 0) { typeaheadTooltip = ( @@ -1292,6 +1306,10 @@ const userSearchIndex = useSelector(userStoreMentionSearchIndex); + const chatMentionSearchIndex = useThreadChatMentionSearchIndex( + props.threadInfo, + ); + const { parentThreadID } = props.threadInfo; const parentThreadInfo = useSelector(state => parentThreadID ? threadInfoSelector(state)[parentThreadID] : null, @@ -1336,6 +1354,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 @@ -1,11 +1,12 @@ // @flow import { oldValidUsernameRegexString } from 'lib/shared/account-utils.js'; +import { validChatNameRegexString } from 'lib/shared/thread-utils.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 { nativeMentionTypeaheadRegex };