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, + ChatMentionCandidates, + ResolvedThreadInfo, } from '../types/thread-types.js'; export type TypeaheadMatchedStrings = { @@ -41,6 +43,10 @@ const chatMentionRegexString = `^(? { + 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, @@ -122,9 +143,11 @@ isUserMentioned, extractUserMentionsFromText, getMentionTypeaheadUserSuggestions, + getMentionTypeaheadChatSuggestions, getNewTextAndSelection, getTypeaheadRegexMatches, getUserMentionsCandidates, chatMentionRegex, + encodeChatMentionText, decodeChatMentionText, }; 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,9 +38,11 @@ import { useEditMessage } from 'lib/shared/edit-messages-utils.js'; import { getMentionTypeaheadUserSuggestions, + getMentionTypeaheadChatSuggestions, getTypeaheadRegexMatches, type Selection, getUserMentionsCandidates, + encodeChatMentionText, } from 'lib/shared/mention-utils.js'; import { localIDPrefix, @@ -58,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'; @@ -70,11 +73,13 @@ } from 'lib/types/message-types.js'; import type { Dispatch } from 'lib/types/redux-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; -import { - type ThreadInfo, - type ClientThreadJoinRequest, - type ThreadJoinPayload, - type RelativeMemberInfo, +import type { + ThreadInfo, + ResolvedThreadInfo, + ClientThreadJoinRequest, + ThreadJoinPayload, + RelativeMemberInfo, + ChatMentionCandidates, } from 'lib/types/thread-types.js'; import { type UserInfos } from 'lib/types/user-types.js'; import { @@ -91,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 @@ -171,6 +177,8 @@ +inputState: ?InputState, +userSearchIndex: SentencePrefixSearchIndex, +userMentionsCandidates: $ReadOnlyArray, + +chatMentionSearchIndex: SentencePrefixSearchIndex, + +chatMentionCandidates: ChatMentionCandidates, +parentThreadInfo: ?ThreadInfo, +editedMessagePreview: ?MessagePreviewResult, +editedMessageInfo: ?MessageInfo, @@ -498,20 +506,37 @@ } typeaheadTooltipButton = ({ item, suggestionText, styles }) => { + let text = suggestionText; + let avatarComponent = null; + if (item.type === 'user') { + text = suggestionText; + avatarComponent = ; + } else if (item.type === 'chat') { + text = `@${item.chat.uiName}`; + avatarComponent = ; + } return ( <> - + {avatarComponent} - {suggestionText} + {text} ); }; typeaheadTooltipSuggestionTextExtractor = ( - item: RelativeMemberInfo, + item: + | { type: 'user', user: RelativeMemberInfo } + | { type: 'chat', chat: ResolvedThreadInfo }, ): string => { - return `@${stringForUserExplicit(item)}`; + if (item.type === 'user') { + return `@${stringForUserExplicit(item.user)}`; + } + if (item.type === 'chat') { + return `@[[${item.chat.id}:${encodeChatMentionText(item.chat.uiName)}]]`; + } + return ''; }; render() { @@ -580,13 +605,22 @@ this.props.viewerID, typeaheadMatchedStrings.textPrefix, ); + 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 (suggestedUsers.length > 0) { + if (suggestions.length > 0) { typeaheadTooltip = ( parentThreadID ? threadInfoSelector(state)[parentThreadID] : null, @@ -1316,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 };