diff --git a/lib/shared/typeahead-utils.js b/lib/shared/typeahead-utils.js new file mode 100644 --- /dev/null +++ b/lib/shared/typeahead-utils.js @@ -0,0 +1,26 @@ +// @flow + +import type { RelativeMemberInfo } from '../types/thread-types'; +import SearchIndex from './search-index'; +import { threadOtherMembers } from './thread-utils'; +import { stringForUserExplicit } from './user-utils'; + +function getTypeaheadUserSuggestions( + userSearchIndex: SearchIndex, + threadMembers: $ReadOnlyArray, + viewerID: ?string, + typedUsernamePrefix: string, +): $ReadOnlyArray { + const userIDs = userSearchIndex.getSearchResults(typedUsernamePrefix); + const usersInThread = threadOtherMembers(threadMembers, viewerID); + + return usersInThread + .filter( + user => typedUsernamePrefix.length === 0 || userIDs.includes(user.id), + ) + .sort((userA, userB) => + stringForUserExplicit(userA).localeCompare(stringForUserExplicit(userB)), + ); +} + +export { getTypeaheadUserSuggestions }; 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 @@ -51,9 +51,10 @@ import { useSelector } from '../redux/redux-utils'; import { nonThreadCalendarQuery } from '../selectors/nav-selectors'; import SWMansionIcon from '../SWMansionIcon.react'; +import { webTypeaheadRegex } from '../utils/typeahead-utils'; import css from './chat-input-bar.css'; import TypeaheadTooltip from './typeahead-tooltip.react'; -import { typeaheadRegex } from './typeahead-utils'; + type BaseProps = { +threadInfo: ThreadInfo, +inputState: InputState, @@ -77,7 +78,6 @@ +typeaheadMatchedStrings: ?TypeaheadMatchedStrings, }; export type TypeaheadMatchedStrings = { - +entireText: string, +textBeforeAtSymbol: string, +usernamePrefix: string, }; @@ -573,7 +573,7 @@ () => inputSliceEndingAtCursor.length === props.inputState.draft.length || /\s/.test(props.inputState.draft[props.inputState.textCursorPosition]) - ? inputSliceEndingAtCursor.match(typeaheadRegex) + ? inputSliceEndingAtCursor.match(webTypeaheadRegex) : null, [ inputSliceEndingAtCursor, @@ -586,9 +586,9 @@ () => typeaheadRegexMatches !== null ? { - entireText: typeaheadRegexMatches[0], - textBeforeAtSymbol: typeaheadRegexMatches[1], - usernamePrefix: typeaheadRegexMatches[2], + textBeforeAtSymbol: + typeaheadRegexMatches.groups?.textPrefix ?? '', + usernamePrefix: typeaheadRegexMatches.groups?.username ?? '', } : null, [typeaheadRegexMatches], 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 @@ -4,20 +4,19 @@ import * as React from 'react'; import SearchIndex from 'lib/shared/search-index'; -import { threadOtherMembers } from 'lib/shared/thread-utils'; +import { getTypeaheadUserSuggestions } from 'lib/shared/typeahead-utils'; import type { RelativeMemberInfo } from 'lib/types/thread-types'; import type { InputState } from '../input/input-state'; -import { type TypeaheadMatchedStrings } from './chat-input-bar.react'; -import css from './typeahead-tooltip.css'; import { getTypeaheadOverlayScroll, getTypeaheadTooltipActions, getTypeaheadTooltipButtons, getTypeaheadTooltipPosition, - getTypeaheadUserSuggestions, getTypeaheadChosenActionPosition, -} from './typeahead-utils'; +} from '../utils/typeahead-utils'; +import { type TypeaheadMatchedStrings } from './chat-input-bar.react'; +import css from './typeahead-tooltip.css'; export type TypeaheadTooltipProps = { +inputState: InputState, @@ -38,13 +37,9 @@ matchedStrings, } = props; - const { - entireText: matchedText, - textBeforeAtSymbol: matchedTextBeforeAtSymbol, - usernamePrefix: matchedUsernamePrefix, - } = matchedStrings; + const { textBeforeAtSymbol, usernamePrefix } = matchedStrings; - const typedPrefix = matchedUsernamePrefix ?? ''; + const typedUsernamePrefix = usernamePrefix ?? ''; const [isAnimation, setIsAnimation] = React.useState(false); const overlayRef = React.useRef(); @@ -59,10 +54,11 @@ () => getTypeaheadUserSuggestions( userSearchIndex, - threadOtherMembers(threadMembers, viewerID), - typedPrefix, + threadMembers, + viewerID, + typedUsernamePrefix, ), - [userSearchIndex, threadMembers, viewerID, typedPrefix], + [userSearchIndex, threadMembers, viewerID, typedUsernamePrefix], ); const actions = React.useMemo( @@ -72,27 +68,23 @@ inputStateSetDraft: inputState.setDraft, inputStateSetTextCursorPosition: inputState.setTextCursorPosition, suggestedUsers, - matchedTextBeforeAtSymbol, - matchedText, + textBeforeAtSymbol, + typedUsernamePrefix, }), [ inputState.draft, inputState.setDraft, inputState.setTextCursorPosition, suggestedUsers, - matchedTextBeforeAtSymbol, - matchedText, + textBeforeAtSymbol, + typedUsernamePrefix, ], ); const tooltipPosition = React.useMemo( () => - getTypeaheadTooltipPosition( - textarea, - actions.length, - matchedTextBeforeAtSymbol, - ), - [textarea, actions.length, matchedTextBeforeAtSymbol], + getTypeaheadTooltipPosition(textarea, actions.length, textBeforeAtSymbol), + [textarea, actions.length, textBeforeAtSymbol], ); const tooltipPositionStyle = React.useMemo( diff --git a/web/chat/typeahead-utils.js b/web/utils/typeahead-utils.js rename from web/chat/typeahead-utils.js rename to web/utils/typeahead-utils.js --- a/web/chat/typeahead-utils.js +++ b/web/utils/typeahead-utils.js @@ -4,16 +4,15 @@ import * as React from 'react'; import { oldValidUsernameRegexString } from 'lib/shared/account-utils'; -import SearchIndex from 'lib/shared/search-index'; import { stringForUserExplicit } from 'lib/shared/user-utils'; import type { RelativeMemberInfo } from 'lib/types/thread-types'; +import { typeaheadStyle } from '../chat/chat-constants'; +import css from '../chat/typeahead-tooltip.css'; import Button from '../components/button.react'; import type { TypeaheadState } from '../input/input-state'; -import { typeaheadStyle } from './chat-constants'; -import css from './typeahead-tooltip.css'; -const typeaheadRegex: RegExp = new RegExp( +const webTypeaheadRegex: RegExp = new RegExp( `(?(?:^(?:.|\n)*\\s+)|^)@(?${oldValidUsernameRegexString})?$`, ); @@ -28,20 +27,6 @@ +left: number, }; -function getTypeaheadUserSuggestions( - userSearchIndex: SearchIndex, - usersInThread: $ReadOnlyArray, - typedPrefix: string, -): $ReadOnlyArray { - const userIDs = userSearchIndex.getSearchResults(typedPrefix); - - return usersInThread - .filter(user => typedPrefix.length === 0 || userIDs.includes(user.id)) - .sort((userA, userB) => - stringForUserExplicit(userA).localeCompare(stringForUserExplicit(userB)), - ); -} - function getCaretOffsets( textarea: HTMLTextAreaElement, text: string, @@ -94,8 +79,8 @@ inputStateSetDraft: (draft: string) => void, inputStateSetTextCursorPosition: (newPosition: number) => void, suggestedUsers: $ReadOnlyArray, - matchedTextBeforeAtSymbol: string, - matchedText: string, + textBeforeAtSymbol: string, + typedUsernamePrefix: string, }; function getTypeaheadTooltipActions( @@ -106,8 +91,8 @@ inputStateSetDraft, inputStateSetTextCursorPosition, suggestedUsers, - matchedTextBeforeAtSymbol, - matchedText, + textBeforeAtSymbol, + typedUsernamePrefix, } = params; return suggestedUsers .filter( @@ -116,9 +101,12 @@ .map(suggestedUser => ({ key: suggestedUser.id, onClick: () => { - const newPrefixText = matchedTextBeforeAtSymbol; + const newPrefixText = textBeforeAtSymbol; + + const totalMatchLength = + textBeforeAtSymbol.length + typedUsernamePrefix.length + 1; // 1 for @ char - let newSuffixText = inputStateDraft.slice(matchedText.length); + let newSuffixText = inputStateDraft.slice(totalMatchLength); newSuffixText = (newSuffixText[0] !== ' ' ? ' ' : '') + newSuffixText; const newText = @@ -195,11 +183,11 @@ function getTypeaheadTooltipPosition( textarea: HTMLTextAreaElement, actionsLength: number, - matchedTextBefore: string, + textBeforeAtSymbol: string, ): TooltipPosition { const { caretTopOffset, caretLeftOffset } = getCaretOffsets( textarea, - matchedTextBefore, + textBeforeAtSymbol, ); const textareaBoundingClientRect = textarea.getBoundingClientRect(); @@ -234,8 +222,7 @@ } export { - typeaheadRegex, - getTypeaheadUserSuggestions, + webTypeaheadRegex, getCaretOffsets, getTypeaheadTooltipActions, getTypeaheadTooltipButtons,