diff --git a/lib/shared/typeahead-utils.js b/lib/shared/typeahead-utils.js --- a/lib/shared/typeahead-utils.js +++ b/lib/shared/typeahead-utils.js @@ -15,6 +15,20 @@ +end: number, }; +function getTypeaheadRegexMatches( + text: string, + selection: Selection, + regex: RegExp, +): null | RegExp$matchResult { + if ( + selection.start === selection.end && + (selection.start === text.length || /\s/.test(text[selection.end])) + ) { + return text.slice(0, selection.start).match(regex); + } + return null; +} + function getTypeaheadUserSuggestions( userSearchIndex: SearchIndex, threadMembers: $ReadOnlyArray, @@ -54,4 +68,8 @@ return { newText, newSelectionStart }; } -export { getTypeaheadUserSuggestions, getNewTextAndSelection }; +export { + getTypeaheadUserSuggestions, + getNewTextAndSelection, + getTypeaheadRegexMatches, +}; diff --git a/native/chat/chat-input-bar.react.js b/native/chat/chat-input-bar.react.js --- a/native/chat/chat-input-bar.react.js +++ b/native/chat/chat-input-bar.react.js @@ -44,7 +44,11 @@ draftKeyFromThreadID, colorIsDark, } from 'lib/shared/thread-utils'; -import type { Selection } from 'lib/shared/typeahead-utils'; +import { + getTypeaheadUserSuggestions, + getTypeaheadRegexMatches, + type Selection, +} from 'lib/shared/typeahead-utils'; import type { CalendarQuery } from 'lib/types/entry-types'; import type { LoadingStatus } from 'lib/types/loading-types'; import type { PhotoPaste } from 'lib/types/media-types'; @@ -89,8 +93,10 @@ import type { LayoutEvent } from '../types/react-native'; import { type AnimatedViewStyle, AnimatedView } from '../types/styles'; import { runTiming } from '../utils/animation-utils'; +import { nativeTypeaheadRegex } from '../utils/typeahead-utils'; import { ChatContext } from './chat-context'; import type { ChatNavigationProp } from './chat.react'; +import TypeaheadTooltip from './typeahead-tooltip.react'; /* eslint-disable import/no-named-as-default-member */ const { @@ -506,6 +512,56 @@ ); } + // we only try to match if there is end of text or whitespace after cursor + let typeaheadRegexMatches = null; + + if (this.androidPreviousText) { + typeaheadRegexMatches = getTypeaheadRegexMatches( + this.androidPreviousText, + this.state.selection, + nativeTypeaheadRegex, + ); + } else if (this.iosPreviousSelection) { + typeaheadRegexMatches = getTypeaheadRegexMatches( + this.state.text, + this.iosPreviousSelection, + nativeTypeaheadRegex, + ); + } else { + typeaheadRegexMatches = getTypeaheadRegexMatches( + this.state.text, + this.state.selection, + nativeTypeaheadRegex, + ); + } + + let typeaheadTooltip = null; + + if (typeaheadRegexMatches) { + const typeaheadMatchedStrings = { + textBeforeAtSymbol: typeaheadRegexMatches[1] ?? '', + usernamePrefix: typeaheadRegexMatches[4] ?? '', + }; + + const suggestedUsers = getTypeaheadUserSuggestions( + this.props.userSearchIndex, + this.props.threadMembers, + this.props.viewerID, + typeaheadMatchedStrings.usernamePrefix, + ); + + if (suggestedUsers.length > 0) { + typeaheadTooltip = ( + + ); + } + } + let content; const defaultMembersAreVoiced = checkIfDefaultMembersAreVoiced( this.props.threadInfo, @@ -551,6 +607,7 @@ style={this.props.styles.container} onLayout={this.props.onInputBarLayout} > + {typeaheadTooltip} {joinButton} {content} {keyboardInputHost} diff --git a/native/chat/typeahead-tooltip.react.js b/native/chat/typeahead-tooltip.react.js --- a/native/chat/typeahead-tooltip.react.js +++ b/native/chat/typeahead-tooltip.react.js @@ -6,13 +6,13 @@ import { type TypeaheadMatchedStrings, + type Selection, getNewTextAndSelection, } from 'lib/shared/typeahead-utils'; import type { RelativeMemberInfo } from 'lib/types/thread-types'; import Button from '../components/button.react'; import { useStyles } from '../themes/colors'; -import type { Selection } from './chat-input-bar.react'; export type TypeaheadTooltipProps = { +text: string,