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,8 +17,9 @@ getMentionTypeaheadUserSuggestions, getTypeaheadRegexMatches, getUserMentionsCandidates, + type MentionTypeaheadSuggestionItem, + type TypeaheadMatchedStrings, } from 'lib/shared/mention-utils.js'; -import type { TypeaheadMatchedStrings } from 'lib/shared/mention-utils.js'; import { localIDPrefix, trimMessage } from 'lib/shared/message-utils.js'; import { threadHasPermission, @@ -35,7 +36,6 @@ type ThreadInfo, type ClientThreadJoinRequest, type ThreadJoinPayload, - type RelativeMemberInfo, } from 'lib/types/thread-types.js'; import { type UserInfos } from 'lib/types/user-types.js'; import { @@ -56,7 +56,11 @@ import Multimedia from '../media/multimedia.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { nonThreadCalendarQuery } from '../selectors/nav-selectors.js'; -import { webMentionTypeaheadRegex } from '../utils/typeahead-utils.js'; +import { + webMentionTypeaheadRegex, + getMentionTypeaheadTooltipActions, + getMentionTypeaheadTooltipButtons, +} from '../utils/typeahead-utils.js'; type BaseProps = { +threadInfo: ThreadInfo, @@ -74,7 +78,7 @@ +dispatchActionPromise: DispatchActionPromise, +joinThread: (request: ClientThreadJoinRequest) => Promise, +typeaheadMatchedStrings: ?TypeaheadMatchedStrings, - +suggestedUsers: $ReadOnlyArray, + +suggestions: $ReadOnlyArray, +parentThreadInfo: ?ThreadInfo, }; @@ -364,7 +368,7 @@ let typeaheadTooltip; if ( this.props.inputState.typeaheadState.canBeVisible && - this.props.suggestedUsers.length > 0 && + this.props.suggestions.length > 0 && this.props.typeaheadMatchedStrings && this.textarea ) { @@ -373,7 +377,9 @@ inputState={this.props.inputState} textarea={this.textarea} matchedStrings={this.props.typeaheadMatchedStrings} - suggestedUsers={this.props.suggestedUsers} + suggestions={this.props.suggestions} + typeaheadTooltipActionsGetter={getMentionTypeaheadTooltipActions} + typeaheadTooltipButtonsGetter={getMentionTypeaheadTooltipButtons} /> ); } @@ -621,23 +627,22 @@ props.inputState.typeaheadState.keepUpdatingThreadMembers, ]); - const suggestedUsers: $ReadOnlyArray = - React.useMemo(() => { - if (!typeaheadMatchedStrings) { - return []; - } - return getMentionTypeaheadUserSuggestions( - userSearchIndex, - props.inputState.typeaheadState.frozenUserMentionsCandidates, - viewerID, - typeaheadMatchedStrings.textPrefix, - ).map(suggestion => suggestion.userInfo); - }, [ + const suggestedUsers = React.useMemo(() => { + if (!typeaheadMatchedStrings) { + return []; + } + return getMentionTypeaheadUserSuggestions( userSearchIndex, props.inputState.typeaheadState.frozenUserMentionsCandidates, viewerID, - typeaheadMatchedStrings, - ]); + typeaheadMatchedStrings.textPrefix, + ); + }, [ + userSearchIndex, + props.inputState.typeaheadState.frozenUserMentionsCandidates, + viewerID, + typeaheadMatchedStrings, + ]); return ( ); diff --git a/web/chat/typeahead-tooltip.react.js b/web/chat/typeahead-tooltip.react.js --- a/web/chat/typeahead-tooltip.react.js +++ b/web/chat/typeahead-tooltip.react.js @@ -3,28 +3,47 @@ import classNames from 'classnames'; import * as React from 'react'; -import type { TypeaheadMatchedStrings } from 'lib/shared/mention-utils.js'; -import type { RelativeMemberInfo } from 'lib/types/thread-types.js'; +import type { + TypeaheadMatchedStrings, + TypeaheadTooltipActionItem, +} from 'lib/shared/mention-utils.js'; import { leastPositiveResidue } from 'lib/utils/math-utils.js'; import css from './typeahead-tooltip.css'; import type { InputState } from '../input/input-state.js'; +import type { + GetTypeaheadTooltipActionsParams, + GetMentionTypeaheadTooltipButtonsParams, +} from '../utils/typeahead-utils.js'; import { getTypeaheadOverlayScroll, - getTypeaheadTooltipActions, - getTypeaheadTooltipButtons, getTypeaheadTooltipPosition, } from '../utils/typeahead-utils.js'; -export type TypeaheadTooltipProps = { +export type TypeaheadTooltipProps = { +inputState: InputState, +textarea: HTMLTextAreaElement, +matchedStrings: TypeaheadMatchedStrings, - +suggestedUsers: $ReadOnlyArray, + +suggestions: $ReadOnlyArray, + +typeaheadTooltipActionsGetter: ( + GetTypeaheadTooltipActionsParams, + ) => $ReadOnlyArray>, + +typeaheadTooltipButtonsGetter: ( + GetMentionTypeaheadTooltipButtonsParams, + ) => React.Node, }; -function TypeaheadTooltip(props: TypeaheadTooltipProps): React.Node { - const { inputState, textarea, matchedStrings, suggestedUsers } = props; +function TypeaheadTooltip( + props: TypeaheadTooltipProps, +): React.Node { + const { + inputState, + textarea, + matchedStrings, + suggestions, + typeaheadTooltipActionsGetter, + typeaheadTooltipButtonsGetter, + } = props; const { textBeforeAtSymbol, textPrefix } = matchedStrings; @@ -38,7 +57,7 @@ React.useEffect(() => { setChosenPositionInOverlay(0); - }, [suggestedUsers]); + }, [suggestions]); React.useEffect(() => { setIsVisibleForAnimation(true); @@ -57,11 +76,11 @@ const actions = React.useMemo( () => - getTypeaheadTooltipActions({ + typeaheadTooltipActionsGetter({ inputStateDraft: inputState.draft, inputStateSetDraft: inputState.setDraft, inputStateSetTextCursorPosition: inputState.setTextCursorPosition, - suggestedUsers, + suggestions, textBeforeAtSymbol, textPrefix, }), @@ -69,9 +88,10 @@ inputState.draft, inputState.setDraft, inputState.setTextCursorPosition, - suggestedUsers, + suggestions, textBeforeAtSymbol, textPrefix, + typeaheadTooltipActionsGetter, ], ); @@ -91,12 +111,12 @@ const tooltipButtons = React.useMemo( () => - getTypeaheadTooltipButtons( + typeaheadTooltipButtonsGetter({ setChosenPositionInOverlay, chosenPositionInOverlay, actions, - ), - [setChosenPositionInOverlay, actions, chosenPositionInOverlay], + }), + [typeaheadTooltipButtonsGetter, chosenPositionInOverlay, actions], ); const close = React.useCallback(() => { @@ -164,7 +184,7 @@ } }, [chosenPositionInOverlay]); - if (suggestedUsers.length === 0) { + if (suggestions.length === 0) { return null; } 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 @@ -4,10 +4,13 @@ import * as React from 'react'; import { oldValidUsernameRegexString } from 'lib/shared/account-utils.js'; -import { getNewTextAndSelection } from 'lib/shared/mention-utils.js'; +import { + getNewTextAndSelection, + type MentionTypeaheadSuggestionItem, + type TypeaheadTooltipActionItem, +} from 'lib/shared/mention-utils.js'; import { stringForUserExplicit } from 'lib/shared/user-utils.js'; import type { SetState } from 'lib/types/hook-types.js'; -import type { RelativeMemberInfo } from 'lib/types/thread-types.js'; import UserAvatar from '../avatars/user-avatar.react.js'; import { typeaheadStyle } from '../chat/chat-constants.js'; @@ -18,12 +21,6 @@ `(?(?:^(?:.|\n)*\\s+)|^)@(?${oldValidUsernameRegexString})?$`, ); -export type TypeaheadTooltipAction = { - +key: string, - +execute: () => mixed, - +actionButtonContent: { +userID: string, +username: string }, -}; - export type TooltipPosition = { +top: number, +left: number, @@ -76,55 +73,67 @@ caretLeftOffset, }; } -export type GetTypeaheadTooltipActionsParams = { +export type GetTypeaheadTooltipActionsParams = { +inputStateDraft: string, +inputStateSetDraft: (draft: string) => mixed, +inputStateSetTextCursorPosition: (newPosition: number) => mixed, - +suggestedUsers: $ReadOnlyArray, + +suggestions: $ReadOnlyArray, +textBeforeAtSymbol: string, +textPrefix: string, }; -function getTypeaheadTooltipActions( - params: GetTypeaheadTooltipActionsParams, -): $ReadOnlyArray { +function getMentionTypeaheadTooltipActions( + params: GetTypeaheadTooltipActionsParams, +): $ReadOnlyArray> { const { inputStateDraft, inputStateSetDraft, inputStateSetTextCursorPosition, - suggestedUsers, + suggestions, textBeforeAtSymbol, textPrefix, } = params; - return suggestedUsers - .filter( - suggestedUser => stringForUserExplicit(suggestedUser) !== 'anonymous', - ) - .map(suggestedUser => ({ - key: suggestedUser.id, - execute: () => { - const { newText, newSelectionStart } = getNewTextAndSelection( - textBeforeAtSymbol, - inputStateDraft, - textPrefix, - stringForUserExplicit(suggestedUser), - ); - - inputStateSetDraft(newText); - inputStateSetTextCursorPosition(newSelectionStart); - }, - actionButtonContent: { - userID: suggestedUser.id, - username: stringForUserExplicit(suggestedUser), - }, - })); + const actions = []; + for (const suggestion of suggestions) { + if (suggestion.type === 'user') { + const suggestedUser = suggestion.userInfo; + if (stringForUserExplicit(suggestedUser) === 'anonymous') { + continue; + } + const mentionText = `@${stringForUserExplicit(suggestedUser)}`; + actions.push({ + key: suggestedUser.id, + execute: () => { + const { newText, newSelectionStart } = getNewTextAndSelection( + textBeforeAtSymbol, + inputStateDraft, + textPrefix, + mentionText, + ); + + inputStateSetDraft(newText); + inputStateSetTextCursorPosition(newSelectionStart); + }, + actionButtonContent: { + type: 'user', + userInfo: suggestedUser, + }, + }); + } + } + return actions; } -function getTypeaheadTooltipButtons( - setChosenPositionInOverlay: SetState, - chosenPositionInOverlay: number, - actions: $ReadOnlyArray, +export type GetMentionTypeaheadTooltipButtonsParams = { + +setChosenPositionInOverlay: SetState, + +chosenPositionInOverlay: number, + +actions: $ReadOnlyArray>, +}; +function getMentionTypeaheadTooltipButtons( + params: GetMentionTypeaheadTooltipButtonsParams, ): $ReadOnlyArray { + const { setChosenPositionInOverlay, chosenPositionInOverlay, actions } = + params; return actions.map((action, idx) => { const { key, execute, actionButtonContent } = action; const buttonClasses = classNames(css.suggestion, { @@ -137,6 +146,16 @@ setChosenPositionInOverlay(idx); }; + let avatarComponent = null; + let typeaheadButtonText = null; + if (actionButtonContent.type === 'user') { + const suggestedUser = actionButtonContent.userInfo; + avatarComponent = ( + + ); + typeaheadButtonText = `@${stringForUserExplicit(suggestedUser)}`; + } + return ( ); }); @@ -208,8 +227,8 @@ export { webMentionTypeaheadRegex, getCaretOffsets, - getTypeaheadTooltipActions, - getTypeaheadTooltipButtons, + getMentionTypeaheadTooltipActions, + getMentionTypeaheadTooltipButtons, getTypeaheadOverlayScroll, getTypeaheadTooltipPosition, };