Page MenuHomePhabricator

D5722.diff
No OneTemporary

D5722.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,
+};
+export type TypeaheadMatchedStrings = {
+ +entireText: string,
+ +textBeforeAtSymbol: string,
+ +usernamePrefix: string,
};
class ChatInputBar extends React.PureComponent<Props> {
textarea: ?HTMLTextAreaElement;
@@ -118,8 +133,19 @@
return;
}
- if (this.props.threadInfo.id !== prevProps.threadInfo.id && this.textarea) {
+ if (
+ (this.props.threadInfo.id !== prevProps.threadInfo.id ||
+ (inputState.textCursorPosition !== prevInputState.textCursorPosition &&
+ this.textarea?.selectionStart === this.textarea?.selectionEnd)) &&
+ this.textarea
+ ) {
this.textarea.focus();
+
+ this.textarea?.setSelectionRange(
+ inputState.textCursorPosition,
+ inputState.textCursorPosition,
+ 'none',
+ );
}
}
@@ -264,6 +290,8 @@
value={this.props.inputState.draft}
onChange={this.onChangeMessageText}
onKeyDown={this.onKeyDown}
+ onClick={this.onClickTextarea}
+ onSelect={this.onSelectTextarea}
ref={this.textareaRef}
autoFocus
/>
@@ -300,11 +328,26 @@
);
}
+ 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}
+ matchedStrings={this.props.typeaheadMatchedStrings}
+ />
+ );
+ }
+
return (
<div className={css.inputBar}>
{joinButton}
{previews}
{content}
+ {typeaheadTooltip}
</div>
);
}
@@ -318,6 +361,21 @@
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,
+ );
+ };
+
+ onSelectTextarea = (event: SyntheticEvent<HTMLTextAreaElement>) => {
+ this.props.inputState.setTextCursorPosition(
+ event.currentTarget.selectionStart,
+ );
};
focusAndUpdateText = (text: string) => {
@@ -361,8 +419,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 +518,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
+ ? {
+ entireText: typeaheadRegexMatches[0],
+ textBeforeAtSymbol: typeaheadRegexMatches[1],
+ usernamePrefix: typeaheadRegexMatches[2],
+ }
+ : null,
+ [typeaheadRegexMatches],
+ );
return (
<ChatInputBar
@@ -475,6 +566,9 @@
userInfos={userInfos}
dispatchActionPromise={dispatchActionPromise}
joinThread={callJoinThread}
+ userSearchIndex={userSearchIndex}
+ threadMembers={threadMembers}
+ typeaheadMatchedStrings={typeaheadMatchedStrings}
/>
);
},
diff --git a/web/chat/mention-suggestion-tooltip.react.js b/web/chat/mention-suggestion-tooltip.react.js
--- a/web/chat/mention-suggestion-tooltip.react.js
+++ b/web/chat/mention-suggestion-tooltip.react.js
@@ -8,6 +8,7 @@
import Button from '../components/button.react';
import type { InputState } from '../input/input-state';
+import { type TypeaheadMatchedStrings } from './chat-input-bar.react';
import css from './mention-suggestion-tooltip.css';
import {
getTypeaheadTooltipActions,
@@ -21,9 +22,7 @@
+userSearchIndex: SearchIndex,
+threadMembers: $ReadOnlyArray<RelativeMemberInfo>,
+viewerID: ?string,
- +matchedText: string,
- +matchedTextBeforeAtSymbol: string,
- +matchedUsernamePrefix: string,
+ +matchedStrings: TypeaheadMatchedStrings,
};
function MentionSuggestionTooltip(
@@ -35,11 +34,15 @@
userSearchIndex,
threadMembers,
viewerID,
- matchedText,
- matchedTextBeforeAtSymbol,
- matchedUsernamePrefix,
+ matchedStrings,
} = props;
+ const {
+ entireText: matchedText,
+ textBeforeAtSymbol: matchedTextBeforeAtSymbol,
+ usernamePrefix: matchedUsernamePrefix,
+ } = matchedStrings;
+
const typedPrefix = matchedUsernamePrefix ?? '';
const suggestedUsers = React.useMemo(

File Metadata

Mime Type
text/plain
Expires
Mon, Nov 25, 5:48 PM (22 h, 2 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2580581
Default Alt Text
D5722.diff (7 KB)

Event Timeline