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 @@ -10,7 +10,12 @@ newThreadActionTypes, } from 'lib/actions/thread-actions'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; +import { + userStoreSearchIndex, + relativeMemberInfoSelectorForMembersOfThread, +} from 'lib/selectors/user-selectors'; import { localIDPrefix, trimMessage } from 'lib/shared/message-utils'; +import SearchIndex from 'lib/shared/search-index'; import { threadHasPermission, viewerIsMember, @@ -27,6 +32,7 @@ type ClientThreadJoinRequest, type ThreadJoinPayload, } from 'lib/types/thread-types'; +import type { RelativeMemberInfo } from 'lib/types/thread-types'; import { type UserInfos } from 'lib/types/user-types'; import { type DispatchActionPromise, @@ -46,7 +52,8 @@ import { nonThreadCalendarQuery } from '../selectors/nav-selectors'; import SWMansionIcon from '../SWMansionIcon.react'; import css from './chat-input-bar.css'; - +import MentionSuggestionTooltip from './mention-suggestion-tooltip.react'; +import { mentionRegex } from './mention-utils'; type BaseProps = { +threadInfo: ThreadInfo, +inputState: InputState, @@ -65,6 +72,9 @@ +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +joinThread: (request: ClientThreadJoinRequest) => Promise, + +userSearchIndex: SearchIndex, + +threadMembers: $ReadOnlyArray, + +matches: ?RegExp$matchResult, }; class ChatInputBar extends React.PureComponent { textarea: ?HTMLTextAreaElement; @@ -120,6 +130,26 @@ if (this.props.threadInfo.id !== prevProps.threadInfo.id && this.textarea) { this.textarea.focus(); + + this.textarea?.setSelectionRange( + this.props.inputState.textCursorPosition, + this.props.inputState.textCursorPosition, + 'none', + ); + } + + if ( + inputState.textCursorPosition !== prevInputState.textCursorPosition && + this.textarea && + this.textarea.selectionStart === this.textarea.selectionEnd + ) { + this.textarea.focus(); + + this.textarea?.setSelectionRange( + inputState.textCursorPosition, + inputState.textCursorPosition, + 'none', + ); } } @@ -264,6 +294,7 @@ value={this.props.inputState.draft} onChange={this.onChangeMessageText} onKeyDown={this.onKeyDown} + onClick={this.onClickTextarea} ref={this.textareaRef} autoFocus /> @@ -300,11 +331,26 @@ ); } + let typeaheadTooltip; + if (this.props.matches && this.textarea) { + typeaheadTooltip = ( + + ); + } + return (
{joinButton} {previews} {content} + {typeaheadTooltip}
); } @@ -318,6 +364,15 @@ onChangeMessageText = (event: SyntheticEvent) => { this.props.inputState.setDraft(event.currentTarget.value); + this.props.inputState.setTextCursorPosition( + event.currentTarget.selectionStart, + ); + }; + + onClickTextarea = (event: SyntheticEvent) => { + this.props.inputState.setTextCursorPosition( + event.currentTarget.selectionStart, + ); }; focusAndUpdateText = (text: string) => { @@ -361,8 +416,6 @@ const text = trimMessage(this.props.inputState.draft); if (text) { - // TODO we should make the send button appear dynamically - // iff trimmed text is nonempty, just like native this.dispatchTextMessageAction(text, nextLocalID); nextLocalID++; } @@ -462,6 +515,28 @@ const calendarQuery = useSelector(nonThreadCalendarQuery); const dispatchActionPromise = useDispatchActionPromise(); const callJoinThread = useServerCall(joinThread); + const userSearchIndex = useSelector(userStoreSearchIndex); + const threadMembers = useSelector( + relativeMemberInfoSelectorForMembersOfThread(props.threadInfo.id), + ); + + const inputSliceEndingAtCursor = props.inputState.draft.slice( + 0, + props.inputState.textCursorPosition, + ); + // we only try to match if there is end ot text or whitespace after cursor + const matches = React.useMemo( + () => + inputSliceEndingAtCursor.length === props.inputState.draft.length || + /\s/.test(props.inputState.draft[props.inputState.textCursorPosition]) + ? inputSliceEndingAtCursor.match(mentionRegex) + : null, + [ + inputSliceEndingAtCursor, + props.inputState.textCursorPosition, + props.inputState.draft, + ], + ); return ( ); },