diff --git a/web/chat/chat-input-bar.react.js b/web/chat/chat-input-bar.react.js --- a/web/chat/chat-input-bar.react.js +++ b/web/chat/chat-input-bar.react.js @@ -17,6 +17,7 @@ getMentionTypeaheadUserSuggestions, getTypeaheadRegexMatches, getUserMentionsCandidates, + getMentionTypeaheadChatSuggestions, type MentionTypeaheadSuggestionItem, type TypeaheadMatchedStrings, } from 'lib/shared/mention-utils.js'; @@ -27,6 +28,8 @@ threadFrozenDueToViewerBlock, threadActualMembers, checkIfDefaultMembersAreVoiced, + 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'; @@ -578,6 +581,9 @@ const dispatchActionPromise = useDispatchActionPromise(); const callJoinThread = useServerCall(joinThread); const userSearchIndex = useSelector(userStoreMentionSearchIndex); + const chatMentionSearchIndex = useThreadChatMentionSearchIndex( + props.threadInfo, + ); const { parentThreadID } = props.threadInfo; const parentThreadInfo = useSelector(state => @@ -589,6 +595,10 @@ parentThreadInfo, ); + const chatMentionCandidates = useThreadChatMentionCandidates( + props.threadInfo, + ); + const typeaheadRegexMatches = React.useMemo( () => getTypeaheadRegexMatches( @@ -619,29 +629,39 @@ const setter = props.inputState.setTypeaheadState; setter({ frozenUserMentionsCandidates: userMentionsCandidates, + frozenChatMentionsCandidates: chatMentionCandidates, }); } }, [ userMentionsCandidates, props.inputState.setTypeaheadState, props.inputState.typeaheadState.keepUpdatingThreadMembers, + chatMentionCandidates, ]); - const suggestedUsers = React.useMemo(() => { + const suggestions = React.useMemo(() => { if (!typeaheadMatchedStrings) { return []; } - return getMentionTypeaheadUserSuggestions( + const suggestedUsers = getMentionTypeaheadUserSuggestions( userSearchIndex, props.inputState.typeaheadState.frozenUserMentionsCandidates, viewerID, typeaheadMatchedStrings.query, ); + const suggestedChats = getMentionTypeaheadChatSuggestions( + chatMentionSearchIndex, + props.inputState.typeaheadState.frozenChatMentionsCandidates, + typeaheadMatchedStrings.query, + ); + return [...suggestedUsers, ...suggestedChats]; }, [ + typeaheadMatchedStrings, userSearchIndex, props.inputState.typeaheadState.frozenUserMentionsCandidates, + props.inputState.typeaheadState.frozenChatMentionsCandidates, viewerID, - typeaheadMatchedStrings, + chatMentionSearchIndex, ]); return ( @@ -657,7 +677,7 @@ dispatchActionPromise={dispatchActionPromise} joinThread={callJoinThread} typeaheadMatchedStrings={typeaheadMatchedStrings} - suggestions={suggestedUsers} + suggestions={suggestions} parentThreadInfo={parentThreadInfo} /> ); diff --git a/web/input/input-state-container.react.js b/web/input/input-state-container.react.js --- a/web/input/input-state-container.react.js +++ b/web/input/input-state-container.react.js @@ -174,6 +174,7 @@ canBeVisible: false, keepUpdatingThreadMembers: true, frozenUserMentionsCandidates: [], + frozenChatMentionsCandidates: {}, moveChoiceUp: null, moveChoiceDown: null, close: null, diff --git a/web/input/input-state.js b/web/input/input-state.js --- a/web/input/input-state.js +++ b/web/input/input-state.js @@ -9,7 +9,11 @@ type MediaMissionStep, } from 'lib/types/media-types.js'; import type { RawTextMessageInfo } from 'lib/types/messages/text.js'; -import type { ThreadInfo, RelativeMemberInfo } from 'lib/types/thread-types.js'; +import type { + ThreadInfo, + RelativeMemberInfo, + ChatMentionCandidates, +} from 'lib/types/thread-types.js'; export type PendingMultimediaUpload = { +localID: string, @@ -43,6 +47,7 @@ +canBeVisible: boolean, +keepUpdatingThreadMembers: boolean, +frozenUserMentionsCandidates: $ReadOnlyArray, + +frozenChatMentionsCandidates: ChatMentionCandidates, +moveChoiceUp: ?() => void, +moveChoiceDown: ?() => void, +close: ?() => void, diff --git a/web/utils/typeahead-utils.js b/web/utils/typeahead-utils.js --- a/web/utils/typeahead-utils.js +++ b/web/utils/typeahead-utils.js @@ -7,11 +7,13 @@ getNewTextAndSelection, type MentionTypeaheadSuggestionItem, type TypeaheadTooltipActionItem, + getRawChatMention, } from 'lib/shared/mention-utils.js'; import { validChatNameRegexString } from 'lib/shared/thread-utils.js'; import { stringForUserExplicit } from 'lib/shared/user-utils.js'; import type { SetState } from 'lib/types/hook-types.js'; +import ThreadAvatar from '../avatars/thread-avatar.react.js'; import UserAvatar from '../avatars/user-avatar.react.js'; import { typeaheadStyle } from '../chat/chat-constants.js'; import css from '../chat/typeahead-tooltip.css'; @@ -81,7 +83,23 @@ +textBeforeAtSymbol: string, +query: string, }; - +function mentionTypeaheadTooltipActionExecuteHandler({ + textBeforeAtSymbol, + inputStateDraft, + query, + mentionText, + inputStateSetDraft, + inputStateSetTextCursorPosition, +}) { + const { newText, newSelectionStart } = getNewTextAndSelection( + textBeforeAtSymbol, + inputStateDraft, + query, + mentionText, + ); + inputStateSetDraft(newText); + inputStateSetTextCursorPosition(newSelectionStart); +} function getMentionTypeaheadTooltipActions( params: GetTypeaheadTooltipActionsParams, ): $ReadOnlyArray> { @@ -103,22 +121,39 @@ const mentionText = `@${stringForUserExplicit(suggestedUser)}`; actions.push({ key: suggestedUser.id, - execute: () => { - const { newText, newSelectionStart } = getNewTextAndSelection( + execute: () => + mentionTypeaheadTooltipActionExecuteHandler({ textBeforeAtSymbol, inputStateDraft, query, mentionText, - ); - - inputStateSetDraft(newText); - inputStateSetTextCursorPosition(newSelectionStart); - }, + inputStateSetDraft, + inputStateSetTextCursorPosition, + }), actionButtonContent: { type: 'user', userInfo: suggestedUser, }, }); + } else if (suggestion.type === 'chat') { + const suggestedChat = suggestion.threadInfo; + const mentionText = getRawChatMention(suggestedChat); + actions.push({ + key: suggestedChat.id, + execute: () => + mentionTypeaheadTooltipActionExecuteHandler({ + textBeforeAtSymbol, + inputStateDraft, + query, + mentionText, + inputStateSetDraft, + inputStateSetTextCursorPosition, + }), + actionButtonContent: { + type: 'chat', + threadInfo: suggestedChat, + }, + }); } } return actions; @@ -154,6 +189,15 @@ ); typeaheadButtonText = `@${stringForUserExplicit(suggestedUser)}`; + } else if (actionButtonContent.type === 'chat') { + const suggestedChat = actionButtonContent.threadInfo; + avatarComponent = ( + + ); + typeaheadButtonText = `@${suggestedChat.uiName}`; } return (