Page MenuHomePhabricator

D5722.id18844.diff
No OneTemporary

D5722.id18844.diff

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,14 @@
+dispatchActionPromise: DispatchActionPromise,
// async functions that hit server APIs
+joinThread: (request: ClientThreadJoinRequest) => Promise<ThreadJoinPayload>,
+ +userSearchIndex: SearchIndex,
+ +threadMembers: $ReadOnlyArray<RelativeMemberInfo>,
+ +typeaheadMatchedStrings: ?TypeaheadMatchedStrings,
+};
+type TypeaheadMatchedStrings = {
+ matchedText: string,
+ matchedTextBeforeAtSymbol: string,
+ matchedUsernamePrefix: string,
};
class ChatInputBar extends React.PureComponent<Props> {
textarea: ?HTMLTextAreaElement;
@@ -120,6 +135,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 +299,7 @@
value={this.props.inputState.draft}
onChange={this.onChangeMessageText}
onKeyDown={this.onKeyDown}
+ onClick={this.onClickTextarea}
ref={this.textareaRef}
autoFocus
/>
@@ -300,11 +336,32 @@
);
}
+ let typeaheadTooltip;
+ if (this.props.typeaheadMatchedStrings && this.textarea) {
+ typeaheadTooltip = (
+ <MentionSuggestionTooltip
+ inputState={this.props.inputState}
+ textarea={this.textarea}
+ userSearchIndex={this.props.userSearchIndex}
+ threadMembers={this.props.threadMembers}
+ viewerID={this.props.viewerID}
+ matchedText={this.props.typeaheadMatchedStrings.matchedText}
+ matchedTextBeforeAtSymbol={
+ this.props.typeaheadMatchedStrings.matchedTextBeforeAtSymbol
+ }
+ matchedUsernamePrefix={
+ this.props.typeaheadMatchedStrings.matchedUsernamePrefix
+ }
+ />
+ );
+ }
+
return (
<div className={css.inputBar}>
{joinButton}
{previews}
{content}
+ {typeaheadTooltip}
</div>
);
}
@@ -318,6 +375,15 @@
onChangeMessageText = (event: SyntheticEvent<HTMLTextAreaElement>) => {
this.props.inputState.setDraft(event.currentTarget.value);
+ this.props.inputState.setTextCursorPosition(
+ event.currentTarget.selectionStart,
+ );
+ };
+
+ onClickTextarea = (event: SyntheticEvent<HTMLTextAreaElement>) => {
+ this.props.inputState.setTextCursorPosition(
+ event.currentTarget.selectionStart,
+ );
};
focusAndUpdateText = (text: string) => {
@@ -361,8 +427,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 +526,41 @@
const calendarQuery = useSelector(nonThreadCalendarQuery);
const dispatchActionPromise = useDispatchActionPromise();
const callJoinThread = useServerCall(joinThread);
+ const userSearchIndex = useSelector(userStoreSearchIndex);
+ const threadMembers = useSelector(
+ relativeMemberInfoSelectorForMembersOfThread(props.threadInfo.id),
+ );
+
+ const inputSliceEndingAtCursor = React.useMemo(
+ () =>
+ props.inputState.draft.slice(0, props.inputState.textCursorPosition),
+ [props.inputState.draft, props.inputState.textCursorPosition],
+ );
+ // we only try to match if there is end of text or whitespace after cursor
+ const typeaheadRegexMatches = 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,
+ ],
+ );
+
+ const typeaheadMatchedStrings: ?TypeaheadMatchedStrings = React.useMemo(
+ () =>
+ typeaheadRegexMatches !== null
+ ? {
+ matchedText: typeaheadRegexMatches[0],
+ matchedTextBeforeAtSymbol: typeaheadRegexMatches[1],
+ matchedUsernamePrefix: typeaheadRegexMatches[2],
+ }
+ : null,
+ [typeaheadRegexMatches],
+ );
return (
<ChatInputBar
@@ -475,6 +574,9 @@
userInfos={userInfos}
dispatchActionPromise={dispatchActionPromise}
joinThread={callJoinThread}
+ userSearchIndex={userSearchIndex}
+ threadMembers={threadMembers}
+ typeaheadMatchedStrings={typeaheadMatchedStrings}
/>
);
},

File Metadata

Mime Type
text/plain
Expires
Fri, Nov 29, 12:17 PM (21 h, 46 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2596963
Default Alt Text
D5722.id18844.diff (6 KB)

Event Timeline