diff --git a/lib/shared/mention-utils.js b/lib/shared/mention-utils.js
index 273c491ff..ac8ebe5d3 100644
--- a/lib/shared/mention-utils.js
+++ b/lib/shared/mention-utils.js
@@ -1,130 +1,130 @@
 // @flow
 
 import { oldValidUsernameRegexString } from './account-utils.js';
 import SearchIndex from './search-index.js';
 import { threadOtherMembers, chatNameMaxLength } from './thread-utils.js';
 import { stringForUserExplicit } from './user-utils.js';
 import { threadTypes } from '../types/thread-types-enum.js';
 import {
   type ThreadInfo,
   type RelativeMemberInfo,
 } from '../types/thread-types.js';
 import { idSchemaRegex } from '../utils/validation-utils.js';
 
 export type TypeaheadMatchedStrings = {
   +textBeforeAtSymbol: string,
-  +usernamePrefix: string,
+  +query: string,
 };
 
 export type Selection = {
   +start: number,
   +end: number,
 };
 
 // The simple-markdown package already breaks words out for us, and we are
 // supposed to only match when the first word of the input matches
 const markdownMentionRegex: RegExp = new RegExp(
   `^(@(${oldValidUsernameRegexString}))\\b`,
 );
 
 function isMentioned(username: string, text: string): boolean {
   return new RegExp(`\\B@${username}\\b`, 'i').test(text);
 }
 
 const mentionsExtractionRegex = new RegExp(
   `\\B(@(${oldValidUsernameRegexString}))\\b`,
   'g',
 );
 
 const chatMentionRegexString = `^(?<!\\\\)(@\\[\\[(${idSchemaRegex}):(.{1,${chatNameMaxLength}}?)(?<!\\\\)\\]\\])`;
 const chatMentionRegex: RegExp = new RegExp(chatMentionRegexString);
 
 function decodeChatMentionText(text: string): string {
   return text.replace(/\\]/g, ']');
 }
 
 function extractMentionsFromText(text: string): string[] {
   const iterator = text.matchAll(mentionsExtractionRegex);
   return [...iterator].map(matches => matches[2]);
 }
 
 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<RelativeMemberInfo>,
   viewerID: ?string,
   usernamePrefix: string,
 ): $ReadOnlyArray<RelativeMemberInfo> {
   const userIDs = userSearchIndex.getSearchResults(usernamePrefix);
   const usersInThread = threadOtherMembers(threadMembers, viewerID);
 
   return usersInThread
     .filter(user => usernamePrefix.length === 0 || userIDs.includes(user.id))
     .sort((userA, userB) =>
       stringForUserExplicit(userA).localeCompare(stringForUserExplicit(userB)),
     );
 }
 
 function getNewTextAndSelection(
   textBeforeAtSymbol: string,
   entireText: string,
   usernamePrefix: string,
   user: RelativeMemberInfo,
 ): {
   newText: string,
   newSelectionStart: number,
 } {
   const totalMatchLength =
     textBeforeAtSymbol.length + usernamePrefix.length + 1; // 1 for @ char
 
   let newSuffixText = entireText.slice(totalMatchLength);
   newSuffixText = (newSuffixText[0] !== ' ' ? ' ' : '') + newSuffixText;
 
   const newText =
     textBeforeAtSymbol + '@' + stringForUserExplicit(user) + newSuffixText;
 
   const newSelectionStart = newText.length - newSuffixText.length + 1;
 
   return { newText, newSelectionStart };
 }
 
 function getMentionsCandidates(
   threadInfo: ThreadInfo,
   parentThreadInfo: ?ThreadInfo,
 ): $ReadOnlyArray<RelativeMemberInfo> {
   if (threadInfo.type !== threadTypes.SIDEBAR) {
     return threadInfo.members;
   }
   if (parentThreadInfo) {
     return parentThreadInfo.members;
   }
   // This scenario should not occur unless the user logs out while looking at a
   // sidebar. In that scenario, the Redux store may be cleared before ReactNav
   // finishes transitioning away from the previous screen
   return [];
 }
 
 export {
   markdownMentionRegex,
   isMentioned,
   extractMentionsFromText,
   getTypeaheadUserSuggestions,
   getNewTextAndSelection,
   getTypeaheadRegexMatches,
   getMentionsCandidates,
   chatMentionRegex,
   decodeChatMentionText,
 };
diff --git a/native/chat/chat-input-bar.react.js b/native/chat/chat-input-bar.react.js
index ee942b29e..51894cd31 100644
--- a/native/chat/chat-input-bar.react.js
+++ b/native/chat/chat-input-bar.react.js
@@ -1,1419 +1,1419 @@
 // @flow
 
 import Icon from '@expo/vector-icons/Ionicons.js';
 import invariant from 'invariant';
 import _throttle from 'lodash/throttle.js';
 import * as React from 'react';
 import {
   View,
   TextInput,
   TouchableOpacity,
   Platform,
   Text,
   ActivityIndicator,
   TouchableWithoutFeedback,
   NativeAppEventEmitter,
 } from 'react-native';
 import { TextInputKeyboardMangerIOS } from 'react-native-keyboard-input';
 import Animated, {
   EasingNode,
   FadeInDown,
   FadeOutDown,
 } from 'react-native-reanimated';
 import { useDispatch } from 'react-redux';
 
 import {
   moveDraftActionType,
   updateDraftActionType,
 } from 'lib/actions/draft-actions.js';
 import {
   joinThreadActionTypes,
   joinThread,
   newThreadActionTypes,
 } from 'lib/actions/thread-actions.js';
 import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
 import { threadInfoSelector } from 'lib/selectors/thread-selectors.js';
 import { userStoreSearchIndex } from 'lib/selectors/user-selectors.js';
 import { colorIsDark } from 'lib/shared/color-utils.js';
 import { useEditMessage } from 'lib/shared/edit-messages-utils.js';
 import {
   getTypeaheadUserSuggestions,
   getTypeaheadRegexMatches,
   type Selection,
   getMentionsCandidates,
 } from 'lib/shared/mention-utils.js';
 import {
   useNextLocalID,
   trimMessage,
   useMessagePreview,
   messageKey,
   type MessagePreviewResult,
 } from 'lib/shared/message-utils.js';
 import SearchIndex from 'lib/shared/search-index.js';
 import {
   threadHasPermission,
   viewerIsMember,
   threadFrozenDueToViewerBlock,
   threadActualMembers,
   checkIfDefaultMembersAreVoiced,
   draftKeyFromThreadID,
   useThreadChatMentionCandidates,
 } from 'lib/shared/thread-utils.js';
 import type { CalendarQuery } from 'lib/types/entry-types.js';
 import type { LoadingStatus } from 'lib/types/loading-types.js';
 import type { PhotoPaste } from 'lib/types/media-types.js';
 import { messageTypes } from 'lib/types/message-types-enum.js';
 import type {
   SendEditMessageResponse,
   MessageInfo,
 } from 'lib/types/message-types.js';
 import type { Dispatch } from 'lib/types/redux-types.js';
 import { threadPermissions } from 'lib/types/thread-permission-types.js';
 import {
   type ThreadInfo,
   type ClientThreadJoinRequest,
   type ThreadJoinPayload,
   type RelativeMemberInfo,
 } from 'lib/types/thread-types.js';
 import { type UserInfos } from 'lib/types/user-types.js';
 import {
   type DispatchActionPromise,
   useServerCall,
   useDispatchActionPromise,
 } from 'lib/utils/action-utils.js';
 
 import { ChatContext } from './chat-context.js';
 import type { ChatNavigationProp } from './chat.react.js';
 import {
   MessageEditingContext,
   type MessageEditingContextType,
 } from './message-editing-context.react.js';
 import type { RemoveEditMode } from './message-list-types.js';
 import TypeaheadTooltip from './typeahead-tooltip.react.js';
 import Button from '../components/button.react.js';
 // eslint-disable-next-line import/extensions
 import ClearableTextInput from '../components/clearable-text-input.react';
 import type { SyncedSelectionData } from '../components/selectable-text-input.js';
 // eslint-disable-next-line import/extensions
 import SelectableTextInput from '../components/selectable-text-input.react';
 import SingleLine from '../components/single-line.react.js';
 import SWMansionIcon from '../components/swmansion-icon.react.js';
 import {
   type InputState,
   InputStateContext,
   type EditInputBarMessageParameters,
 } from '../input/input-state.js';
 import KeyboardInputHost from '../keyboard/keyboard-input-host.react.js';
 import {
   type KeyboardState,
   KeyboardContext,
 } from '../keyboard/keyboard-state.js';
 import { getKeyboardHeight } from '../keyboard/keyboard.js';
 import { getDefaultTextMessageRules } from '../markdown/rules.react.js';
 import {
   nonThreadCalendarQuery,
   activeThreadSelector,
 } from '../navigation/nav-selectors.js';
 import { NavContext } from '../navigation/navigation-context.js';
 import { OverlayContext } from '../navigation/overlay-context.js';
 import type { OverlayContextType } from '../navigation/overlay-context.js';
 import {
   type NavigationRoute,
   ChatCameraModalRouteName,
   ImagePasteModalRouteName,
 } from '../navigation/route-names.js';
 import { useSelector } from '../redux/redux-utils.js';
 import { type Colors, useStyles, useColors } from '../themes/colors.js';
 import type { LayoutEvent } from '../types/react-native.js';
 import { type AnimatedViewStyle, AnimatedView } from '../types/styles.js';
 import Alert from '../utils/alert.js';
 import { runTiming } from '../utils/animation-utils.js';
 import { exitEditAlert } from '../utils/edit-messages-utils.js';
 import { nativeTypeaheadRegex } from '../utils/typeahead-utils.js';
 
 /* eslint-disable import/no-named-as-default-member */
 const { Value, Clock, block, set, cond, neq, sub, interpolateNode, stopClock } =
   Animated;
 /* eslint-enable import/no-named-as-default-member */
 
 const expandoButtonsAnimationConfig = {
   duration: 150,
   easing: EasingNode.inOut(EasingNode.ease),
 };
 const sendButtonAnimationConfig = {
   duration: 150,
   easing: EasingNode.inOut(EasingNode.ease),
 };
 
 type BaseProps = {
   +threadInfo: ThreadInfo,
 };
 type Props = {
   ...BaseProps,
   +viewerID: ?string,
   +draft: string,
   +joinThreadLoadingStatus: LoadingStatus,
   +threadCreationInProgress: boolean,
   +calendarQuery: () => CalendarQuery,
   +nextLocalID: string,
   +userInfos: UserInfos,
   +colors: Colors,
   +styles: typeof unboundStyles,
   +onInputBarLayout?: (event: LayoutEvent) => mixed,
   +openCamera: () => mixed,
   +isActive: boolean,
   +keyboardState: ?KeyboardState,
   +dispatch: Dispatch,
   +dispatchActionPromise: DispatchActionPromise,
   +joinThread: (request: ClientThreadJoinRequest) => Promise<ThreadJoinPayload>,
   +inputState: ?InputState,
   +userSearchIndex: SearchIndex,
   +mentionsCandidates: $ReadOnlyArray<RelativeMemberInfo>,
   +parentThreadInfo: ?ThreadInfo,
   +editedMessagePreview: ?MessagePreviewResult,
   +editedMessageInfo: ?MessageInfo,
   +editMessage: (
     messageID: string,
     text: string,
   ) => Promise<SendEditMessageResponse>,
   +navigation: ?ChatNavigationProp<'MessageList'>,
   +overlayContext: ?OverlayContextType,
   +messageEditingContext: ?MessageEditingContextType,
 };
 type State = {
   +text: string,
   +textEdited: boolean,
   +buttonsExpanded: boolean,
   +selectionState: SyncedSelectionData,
   +isExitingDuringEditMode: boolean,
 };
 class ChatInputBar extends React.PureComponent<Props, State> {
   textInput: ?React.ElementRef<typeof TextInput>;
   clearableTextInput: ?ClearableTextInput;
   selectableTextInput: ?React.ElementRef<typeof SelectableTextInput>;
 
   expandoButtonsOpen: Value;
   targetExpandoButtonsOpen: Value;
   expandoButtonsStyle: AnimatedViewStyle;
   cameraRollIconStyle: AnimatedViewStyle;
   cameraIconStyle: AnimatedViewStyle;
   expandIconStyle: AnimatedViewStyle;
 
   sendButtonContainerOpen: Value;
   targetSendButtonContainerOpen: Value;
   sendButtonContainerStyle: AnimatedViewStyle;
 
   clearBeforeRemoveListener: () => void;
   clearFocusListener: () => void;
   clearBlurListener: () => void;
 
   constructor(props: Props) {
     super(props);
     this.state = {
       text: props.draft,
       textEdited: false,
       buttonsExpanded: true,
       selectionState: { text: props.draft, selection: { start: 0, end: 0 } },
       isExitingDuringEditMode: false,
     };
 
     this.setUpActionIconAnimations();
     this.setUpSendIconAnimations();
   }
 
   setUpActionIconAnimations() {
     this.expandoButtonsOpen = new Value(1);
     this.targetExpandoButtonsOpen = new Value(1);
     const prevTargetExpandoButtonsOpen = new Value(1);
     const expandoButtonClock = new Clock();
     const expandoButtonsOpen = block([
       cond(neq(this.targetExpandoButtonsOpen, prevTargetExpandoButtonsOpen), [
         stopClock(expandoButtonClock),
         set(prevTargetExpandoButtonsOpen, this.targetExpandoButtonsOpen),
       ]),
       cond(
         neq(this.expandoButtonsOpen, this.targetExpandoButtonsOpen),
         set(
           this.expandoButtonsOpen,
           runTiming(
             expandoButtonClock,
             this.expandoButtonsOpen,
             this.targetExpandoButtonsOpen,
             true,
             expandoButtonsAnimationConfig,
           ),
         ),
       ),
       this.expandoButtonsOpen,
     ]);
 
     this.cameraRollIconStyle = {
       ...unboundStyles.cameraRollIcon,
       opacity: expandoButtonsOpen,
     };
     this.cameraIconStyle = {
       ...unboundStyles.cameraIcon,
       opacity: expandoButtonsOpen,
     };
 
     const expandoButtonsWidth = interpolateNode(expandoButtonsOpen, {
       inputRange: [0, 1],
       outputRange: [26, 66],
     });
     this.expandoButtonsStyle = {
       ...unboundStyles.expandoButtons,
       width: expandoButtonsWidth,
     };
 
     const expandOpacity = sub(1, expandoButtonsOpen);
     this.expandIconStyle = {
       ...unboundStyles.expandIcon,
       opacity: expandOpacity,
     };
   }
 
   setUpSendIconAnimations() {
     const initialSendButtonContainerOpen = trimMessage(this.props.draft)
       ? 1
       : 0;
     this.sendButtonContainerOpen = new Value(initialSendButtonContainerOpen);
     this.targetSendButtonContainerOpen = new Value(
       initialSendButtonContainerOpen,
     );
     const prevTargetSendButtonContainerOpen = new Value(
       initialSendButtonContainerOpen,
     );
     const sendButtonClock = new Clock();
     const sendButtonContainerOpen = block([
       cond(
         neq(
           this.targetSendButtonContainerOpen,
           prevTargetSendButtonContainerOpen,
         ),
         [
           stopClock(sendButtonClock),
           set(
             prevTargetSendButtonContainerOpen,
             this.targetSendButtonContainerOpen,
           ),
         ],
       ),
       cond(
         neq(this.sendButtonContainerOpen, this.targetSendButtonContainerOpen),
         set(
           this.sendButtonContainerOpen,
           runTiming(
             sendButtonClock,
             this.sendButtonContainerOpen,
             this.targetSendButtonContainerOpen,
             true,
             sendButtonAnimationConfig,
           ),
         ),
       ),
       this.sendButtonContainerOpen,
     ]);
 
     const sendButtonContainerWidth = interpolateNode(sendButtonContainerOpen, {
       inputRange: [0, 1],
       outputRange: [4, 38],
     });
     this.sendButtonContainerStyle = { width: sendButtonContainerWidth };
   }
 
   static mediaGalleryOpen(props: Props) {
     const { keyboardState } = props;
     return !!(keyboardState && keyboardState.mediaGalleryOpen);
   }
 
   static systemKeyboardShowing(props: Props) {
     const { keyboardState } = props;
     return !!(keyboardState && keyboardState.systemKeyboardShowing);
   }
 
   get systemKeyboardShowing() {
     return ChatInputBar.systemKeyboardShowing(this.props);
   }
 
   immediatelyShowSendButton() {
     this.sendButtonContainerOpen.setValue(1);
     this.targetSendButtonContainerOpen.setValue(1);
   }
 
   updateSendButton(currentText: string) {
     if (this.shouldShowTextInput) {
       this.targetSendButtonContainerOpen.setValue(currentText === '' ? 0 : 1);
     } else {
       this.setUpSendIconAnimations();
     }
   }
 
   componentDidMount() {
     const { isActive, navigation } = this.props;
     if (isActive) {
       this.addEditInputMessageListener();
     }
     if (!navigation) {
       return;
     }
     this.clearBeforeRemoveListener = navigation.addListener(
       'beforeRemove',
       this.onNavigationBeforeRemove,
     );
     this.clearFocusListener = navigation.addListener(
       'focus',
       this.onNavigationFocus,
     );
     this.clearBlurListener = navigation.addListener(
       'blur',
       this.onNavigationBlur,
     );
   }
 
   componentWillUnmount() {
     if (this.props.isActive) {
       this.removeEditInputMessageListener();
     }
     if (this.clearBeforeRemoveListener) {
       this.clearBeforeRemoveListener();
     }
     if (this.clearFocusListener) {
       this.clearFocusListener();
     }
     if (this.clearBlurListener) {
       this.clearBlurListener();
     }
   }
 
   componentDidUpdate(prevProps: Props, prevState: State) {
     if (
       this.state.textEdited &&
       this.state.text &&
       this.props.threadInfo.id !== prevProps.threadInfo.id
     ) {
       this.props.dispatch({
         type: moveDraftActionType,
         payload: {
           oldKey: draftKeyFromThreadID(prevProps.threadInfo.id),
           newKey: draftKeyFromThreadID(this.props.threadInfo.id),
         },
       });
     } else if (!this.state.textEdited && this.props.draft !== prevProps.draft) {
       this.setState({ text: this.props.draft });
     }
     if (this.props.isActive && !prevProps.isActive) {
       this.addEditInputMessageListener();
     } else if (!this.props.isActive && prevProps.isActive) {
       this.removeEditInputMessageListener();
     }
 
     const currentText = trimMessage(this.state.text);
     const prevText = trimMessage(prevState.text);
 
     if (
       (currentText === '' && prevText !== '') ||
       (currentText !== '' && prevText === '')
     ) {
       this.updateSendButton(currentText);
     }
 
     const systemKeyboardIsShowing = ChatInputBar.systemKeyboardShowing(
       this.props,
     );
     const systemKeyboardWasShowing =
       ChatInputBar.systemKeyboardShowing(prevProps);
     if (systemKeyboardIsShowing && !systemKeyboardWasShowing) {
       this.hideButtons();
     } else if (!systemKeyboardIsShowing && systemKeyboardWasShowing) {
       this.expandButtons();
     }
 
     const imageGalleryIsOpen = ChatInputBar.mediaGalleryOpen(this.props);
     const imageGalleryWasOpen = ChatInputBar.mediaGalleryOpen(prevProps);
     if (!imageGalleryIsOpen && imageGalleryWasOpen) {
       this.hideButtons();
     } else if (imageGalleryIsOpen && !imageGalleryWasOpen) {
       this.expandButtons();
       this.setIOSKeyboardHeight();
     }
 
     if (
       this.props.messageEditingContext?.editState.editedMessage &&
       !prevProps.messageEditingContext?.editState.editedMessage
     ) {
       this.blockNavigation();
     }
   }
 
   addEditInputMessageListener() {
     invariant(
       this.props.inputState,
       'inputState should be set in addEditInputMessageListener',
     );
     this.props.inputState.addEditInputMessageListener(this.focusAndUpdateText);
   }
 
   removeEditInputMessageListener() {
     invariant(
       this.props.inputState,
       'inputState should be set in removeEditInputMessageListener',
     );
     this.props.inputState.removeEditInputMessageListener(
       this.focusAndUpdateText,
     );
   }
 
   setIOSKeyboardHeight() {
     if (Platform.OS !== 'ios') {
       return;
     }
     const { textInput } = this;
     if (!textInput) {
       return;
     }
     const keyboardHeight = getKeyboardHeight();
     if (keyboardHeight === null || keyboardHeight === undefined) {
       return;
     }
     TextInputKeyboardMangerIOS.setKeyboardHeight(textInput, keyboardHeight);
   }
 
   get shouldShowTextInput(): boolean {
     if (threadHasPermission(this.props.threadInfo, threadPermissions.VOICED)) {
       return true;
     }
     // If the thread is created by somebody else while the viewer is attempting
     // to create it, the threadInfo might be modified in-place
     // and won't list the viewer as a member,
     // which will end up hiding the input.
     // In this case, we will assume that our creation action
     // will get translated into a join, and as long
     // as members are voiced, we can show the input.
     if (!this.props.threadCreationInProgress) {
       return false;
     }
     return checkIfDefaultMembersAreVoiced(this.props.threadInfo);
   }
 
   render() {
     const isMember = viewerIsMember(this.props.threadInfo);
     const canJoin = threadHasPermission(
       this.props.threadInfo,
       threadPermissions.JOIN_THREAD,
     );
     let joinButton = null;
     const threadColor = `#${this.props.threadInfo.color}`;
     const isEditMode = this.isEditMode();
     if (!isMember && canJoin && !this.props.threadCreationInProgress) {
       let buttonContent;
       if (this.props.joinThreadLoadingStatus === 'loading') {
         buttonContent = (
           <ActivityIndicator
             size="small"
             color="white"
             style={this.props.styles.joinThreadLoadingIndicator}
           />
         );
       } else {
         const textStyle = colorIsDark(this.props.threadInfo.color)
           ? this.props.styles.joinButtonTextLight
           : this.props.styles.joinButtonTextDark;
         buttonContent = (
           <View style={this.props.styles.joinButtonContent}>
             <SWMansionIcon name="plus" style={textStyle} />
             <Text style={textStyle}>Join Chat</Text>
           </View>
         );
       }
       joinButton = (
         <View style={this.props.styles.joinButtonContainer}>
           <Button
             onPress={this.onPressJoin}
             iosActiveOpacity={0.85}
             style={[
               this.props.styles.joinButton,
               { backgroundColor: threadColor },
             ]}
           >
             {buttonContent}
           </Button>
         </View>
       );
     }
 
     const typeaheadRegexMatches = getTypeaheadRegexMatches(
       this.state.selectionState.text,
       this.state.selectionState.selection,
       nativeTypeaheadRegex,
     );
 
     let typeaheadTooltip = null;
 
     if (typeaheadRegexMatches && !isEditMode) {
       const typeaheadMatchedStrings = {
         textBeforeAtSymbol: typeaheadRegexMatches[1] ?? '',
-        usernamePrefix: typeaheadRegexMatches[4] ?? '',
+        query: typeaheadRegexMatches[4] ?? '',
       };
 
       const suggestedUsers = getTypeaheadUserSuggestions(
         this.props.userSearchIndex,
         this.props.mentionsCandidates,
         this.props.viewerID,
-        typeaheadMatchedStrings.usernamePrefix,
+        typeaheadMatchedStrings.query,
       );
 
       if (suggestedUsers.length > 0) {
         typeaheadTooltip = (
           <TypeaheadTooltip
             text={this.state.text}
             matchedStrings={typeaheadMatchedStrings}
             suggestedUsers={suggestedUsers}
             focusAndUpdateTextAndSelection={this.focusAndUpdateTextAndSelection}
           />
         );
       }
     }
 
     let content;
     const defaultMembersAreVoiced = checkIfDefaultMembersAreVoiced(
       this.props.threadInfo,
     );
     if (this.shouldShowTextInput) {
       content = this.renderInput();
     } else if (
       threadFrozenDueToViewerBlock(
         this.props.threadInfo,
         this.props.viewerID,
         this.props.userInfos,
       ) &&
       threadActualMembers(this.props.threadInfo.members).length === 2
     ) {
       content = (
         <Text style={this.props.styles.explanation}>
           You can&rsquo;t send messages to a user that you&rsquo;ve blocked.
         </Text>
       );
     } else if (isMember) {
       content = (
         <Text style={this.props.styles.explanation}>
           You don&rsquo;t have permission to send messages.
         </Text>
       );
     } else if (defaultMembersAreVoiced && canJoin) {
       content = null;
     } else {
       content = (
         <Text style={this.props.styles.explanation}>
           You don&rsquo;t have permission to send messages.
         </Text>
       );
     }
 
     const keyboardInputHost =
       Platform.OS === 'android' ? null : (
         <KeyboardInputHost textInputRef={this.textInput} />
       );
 
     let editedMessage;
     if (isEditMode && this.props.editedMessagePreview) {
       const { message } = this.props.editedMessagePreview;
       editedMessage = (
         <AnimatedView
           style={this.props.styles.editView}
           entering={FadeInDown}
           exiting={FadeOutDown}
         >
           <View style={this.props.styles.editViewContent}>
             <TouchableOpacity
               onPress={this.scrollToEditedMessage}
               activeOpacity={0.4}
             >
               <Text
                 style={[{ color: threadColor }, this.props.styles.editingLabel]}
               >
                 Editing message
               </Text>
               <SingleLine style={this.props.styles.editingMessagePreview}>
                 {message.text}
               </SingleLine>
             </TouchableOpacity>
           </View>
           <SWMansionIcon
             style={this.props.styles.exitEditButton}
             name="cross"
             size={22}
             color={threadColor}
             onPress={this.onPressExitEditMode}
           />
         </AnimatedView>
       );
     }
 
     return (
       <AnimatedView
         style={this.props.styles.container}
         onLayout={this.props.onInputBarLayout}
       >
         {typeaheadTooltip}
         {joinButton}
         {editedMessage}
         {content}
         {keyboardInputHost}
       </AnimatedView>
     );
   }
 
   renderInput() {
     const expandoButton = (
       <TouchableOpacity
         onPress={this.expandButtons}
         activeOpacity={0.4}
         style={this.props.styles.expandButton}
       >
         <AnimatedView style={this.expandIconStyle}>
           <SWMansionIcon
             name="chevron-right"
             size={22}
             color={`#${this.props.threadInfo.color}`}
           />
         </AnimatedView>
       </TouchableOpacity>
     );
     const threadColor = `#${this.props.threadInfo.color}`;
     const expandoButtonsViewStyle = [this.props.styles.innerExpandoButtons];
     if (this.isEditMode()) {
       expandoButtonsViewStyle.push({ display: 'none' });
     }
     return (
       <TouchableWithoutFeedback onPress={this.dismissKeyboard}>
         <View style={this.props.styles.inputContainer}>
           <AnimatedView style={this.expandoButtonsStyle}>
             <View style={expandoButtonsViewStyle}>
               {this.state.buttonsExpanded ? expandoButton : null}
               <TouchableOpacity
                 onPress={this.showMediaGallery}
                 activeOpacity={0.4}
               >
                 <AnimatedView style={this.cameraRollIconStyle}>
                   <SWMansionIcon
                     name="image-1"
                     size={28}
                     color={`#${this.props.threadInfo.color}`}
                   />
                 </AnimatedView>
               </TouchableOpacity>
               <TouchableOpacity
                 onPress={this.props.openCamera}
                 activeOpacity={0.4}
                 disabled={!this.state.buttonsExpanded}
               >
                 <AnimatedView style={this.cameraIconStyle}>
                   <SWMansionIcon
                     name="camera"
                     size={28}
                     color={`#${this.props.threadInfo.color}`}
                   />
                 </AnimatedView>
               </TouchableOpacity>
               {this.state.buttonsExpanded ? null : expandoButton}
             </View>
           </AnimatedView>
           <SelectableTextInput
             allowImagePasteForThreadID={this.props.threadInfo.id}
             value={this.state.text}
             onChangeText={this.updateText}
             selection={this.state.selectionState.selection}
             onUpdateSyncedSelectionData={this.updateSelectionState}
             placeholder="Send a message..."
             placeholderTextColor={this.props.colors.listInputButton}
             multiline={true}
             style={this.props.styles.textInput}
             textInputRef={this.textInputRef}
             clearableTextInputRef={this.clearableTextInputRef}
             ref={this.selectableTextInputRef}
             selectionColor={`#${this.props.threadInfo.color}`}
           />
           <AnimatedView style={this.sendButtonContainerStyle}>
             <TouchableOpacity
               onPress={this.onSend}
               activeOpacity={0.4}
               style={this.props.styles.sendButton}
               disabled={trimMessage(this.state.text) === ''}
             >
               <Icon
                 name="md-send"
                 size={25}
                 style={this.props.styles.sendIcon}
                 color={threadColor}
               />
             </TouchableOpacity>
           </AnimatedView>
         </View>
       </TouchableWithoutFeedback>
     );
   }
 
   textInputRef = (textInput: ?React.ElementRef<typeof TextInput>) => {
     this.textInput = textInput;
   };
 
   clearableTextInputRef = (clearableTextInput: ?ClearableTextInput) => {
     this.clearableTextInput = clearableTextInput;
   };
 
   selectableTextInputRef = (
     selectableTextInput: ?React.ElementRef<typeof SelectableTextInput>,
   ) => {
     this.selectableTextInput = selectableTextInput;
   };
 
   updateText = (text: string) => {
     if (this.state.isExitingDuringEditMode) {
       return;
     }
     this.setState({ text, textEdited: true });
     this.props.messageEditingContext?.setEditedMessageChanged(
       this.isMessageEdited(text),
     );
     if (this.isEditMode()) {
       return;
     }
     this.saveDraft(text);
   };
 
   updateSelectionState: (data: SyncedSelectionData) => void = data => {
     this.setState({ selectionState: data });
   };
 
   saveDraft = _throttle(text => {
     this.props.dispatch({
       type: updateDraftActionType,
       payload: {
         key: draftKeyFromThreadID(this.props.threadInfo.id),
         text,
       },
     });
   }, 400);
 
   focusAndUpdateTextAndSelection = (text: string, selection: Selection) => {
     this.selectableTextInput?.prepareForSelectionMutation(text, selection);
     this.setState({
       text,
       textEdited: true,
       selectionState: { text, selection },
     });
     this.saveDraft(text);
 
     this.focusAndUpdateButtonsVisibility();
   };
 
   focusAndUpdateText = (params: EditInputBarMessageParameters) => {
     const { message: text, mode } = params;
     const currentText = this.state.text;
     if (mode === 'replace') {
       this.updateText(text);
     } else if (!currentText.startsWith(text)) {
       const prependedText = text.concat(currentText);
       this.updateText(prependedText);
     }
 
     this.focusAndUpdateButtonsVisibility();
   };
 
   focusAndUpdateButtonsVisibility = () => {
     const { textInput } = this;
     if (!textInput) {
       return;
     }
 
     this.immediatelyShowSendButton();
     this.immediatelyHideButtons();
     textInput.focus();
   };
 
   onSend = async () => {
     if (!trimMessage(this.state.text)) {
       return;
     }
 
     const editedMessage = this.getEditedMessage();
     if (editedMessage && editedMessage.id) {
       this.editMessage(editedMessage.id, this.state.text);
       return;
     }
 
     this.updateSendButton('');
 
     const { clearableTextInput } = this;
     invariant(
       clearableTextInput,
       'clearableTextInput should be sent in onSend',
     );
     let text = await clearableTextInput.getValueAndReset();
     text = trimMessage(text);
     if (!text) {
       return;
     }
 
     const localID = this.props.nextLocalID;
     const creatorID = this.props.viewerID;
     invariant(creatorID, 'should have viewer ID in order to send a message');
     invariant(
       this.props.inputState,
       'inputState should be set in ChatInputBar.onSend',
     );
 
     this.props.inputState.sendTextMessage(
       {
         type: messageTypes.TEXT,
         localID,
         threadID: this.props.threadInfo.id,
         text,
         creatorID,
         time: Date.now(),
       },
       this.props.threadInfo,
       this.props.parentThreadInfo,
     );
   };
 
   isEditMode = () => {
     const editState = this.props.messageEditingContext?.editState;
     const isThisThread =
       editState?.editedMessage?.threadID === this.props.threadInfo.id;
     return editState && editState.editedMessage !== null && isThisThread;
   };
 
   isMessageEdited = newText => {
     let text = newText ?? this.state.text;
     text = trimMessage(text);
     const originalText = this.props.editedMessageInfo?.text;
     return text !== originalText;
   };
 
   unblockNavigation = () => {
     const { navigation } = this.props;
     if (!navigation) {
       return;
     }
     navigation.setParams({ removeEditMode: null });
   };
 
   removeEditMode: RemoveEditMode = action => {
     const { navigation } = this.props;
     if (!navigation || this.state.isExitingDuringEditMode) {
       return 'ignore_action';
     }
     if (!this.isMessageEdited()) {
       this.unblockNavigation();
       return 'reduce_action';
     }
     const unblockAndDispatch = () => {
       this.unblockNavigation();
       navigation.dispatch(action);
     };
     const onContinueEditing = () => {
       this.props.overlayContext?.resetScrollBlockingModalStatus();
     };
     exitEditAlert({
       onDiscard: unblockAndDispatch,
       onContinueEditing,
     });
     return 'ignore_action';
   };
 
   blockNavigation = () => {
     const { navigation } = this.props;
     if (!navigation || !navigation.isFocused()) {
       return;
     }
     navigation.setParams({
       removeEditMode: this.removeEditMode,
     });
   };
 
   editMessage = async (messageID: string, text: string) => {
     if (!this.isMessageEdited()) {
       this.exitEditMode();
       return;
     }
     text = trimMessage(text);
     try {
       await this.props.editMessage(messageID, text);
       this.exitEditMode();
     } catch (error) {
       Alert.alert(
         'Couldn’t edit the message',
         'Please try again later',
         [{ text: 'OK' }],
         {
           cancelable: true,
         },
       );
     }
   };
 
   getEditedMessage = () => {
     const editState = this.props.messageEditingContext?.editState;
     return editState?.editedMessage;
   };
 
   onPressExitEditMode = () => {
     if (!this.isMessageEdited()) {
       this.exitEditMode();
       return;
     }
     exitEditAlert({
       onDiscard: this.exitEditMode,
     });
   };
 
   scrollToEditedMessage = () => {
     const editedMessage = this.getEditedMessage();
     if (!editedMessage) {
       return;
     }
     const editedMessageKey = messageKey(editedMessage);
     this.props.inputState?.scrollToMessage(editedMessageKey);
   };
 
   exitEditMode = () => {
     this.props.messageEditingContext?.setEditedMessage(null, () => {
       this.unblockNavigation();
       this.updateText(this.props.draft);
       this.focusAndUpdateButtonsVisibility();
       this.updateSendButton(this.props.draft);
     });
   };
 
   onNavigationFocus = () => {
     this.setState({ isExitingDuringEditMode: false });
   };
 
   onNavigationBlur = () => {
     if (!this.isEditMode()) {
       return;
     }
     this.setState(
       { text: this.props.draft, isExitingDuringEditMode: true },
       this.exitEditMode,
     );
   };
 
   onNavigationBeforeRemove = e => {
     if (!this.isEditMode()) {
       return;
     }
     const { action } = e.data;
     e.preventDefault();
     const saveExit = () => {
       this.props.messageEditingContext?.setEditedMessage(null, () => {
         this.setState({ isExitingDuringEditMode: true }, () => {
           if (!this.props.navigation) {
             return;
           }
           this.props.navigation.dispatch(action);
         });
       });
     };
     if (!this.isMessageEdited()) {
       saveExit();
       return;
     }
     exitEditAlert({
       onDiscard: saveExit,
     });
   };
 
   onPressJoin = () => {
     this.props.dispatchActionPromise(joinThreadActionTypes, this.joinAction());
   };
 
   async joinAction() {
     const query = this.props.calendarQuery();
     return await this.props.joinThread({
       threadID: this.props.threadInfo.id,
       calendarQuery: {
         startDate: query.startDate,
         endDate: query.endDate,
         filters: [
           ...query.filters,
           { type: 'threads', threadIDs: [this.props.threadInfo.id] },
         ],
       },
     });
   }
 
   expandButtons = () => {
     if (this.state.buttonsExpanded || this.isEditMode()) {
       return;
     }
     this.targetExpandoButtonsOpen.setValue(1);
     this.setState({ buttonsExpanded: true });
   };
 
   hideButtons() {
     if (
       ChatInputBar.mediaGalleryOpen(this.props) ||
       !this.systemKeyboardShowing ||
       !this.state.buttonsExpanded
     ) {
       return;
     }
     this.targetExpandoButtonsOpen.setValue(0);
     this.setState({ buttonsExpanded: false });
   }
 
   immediatelyHideButtons() {
     this.expandoButtonsOpen.setValue(0);
     this.targetExpandoButtonsOpen.setValue(0);
     this.setState({ buttonsExpanded: false });
   }
 
   showMediaGallery = () => {
     const { keyboardState } = this.props;
     invariant(keyboardState, 'keyboardState should be initialized');
     keyboardState.showMediaGallery(this.props.threadInfo);
   };
 
   dismissKeyboard = () => {
     const { keyboardState } = this.props;
     keyboardState && keyboardState.dismissKeyboard();
   };
 }
 
 const unboundStyles = {
   cameraIcon: {
     paddingBottom: Platform.OS === 'android' ? 11 : 8,
     paddingRight: 5,
   },
   cameraRollIcon: {
     paddingBottom: Platform.OS === 'android' ? 11 : 8,
     paddingRight: 5,
   },
   container: {
     backgroundColor: 'listBackground',
     paddingLeft: Platform.OS === 'android' ? 10 : 5,
   },
   expandButton: {
     bottom: 0,
     position: 'absolute',
     right: 0,
   },
   expandIcon: {
     paddingBottom: Platform.OS === 'android' ? 13 : 11,
     paddingRight: 2,
   },
   expandoButtons: {
     alignSelf: 'flex-end',
   },
   explanation: {
     color: 'listBackgroundSecondaryLabel',
     paddingBottom: 4,
     paddingTop: 1,
     textAlign: 'center',
   },
   innerExpandoButtons: {
     alignItems: 'flex-end',
     alignSelf: 'flex-end',
     flexDirection: 'row',
   },
   inputContainer: {
     flexDirection: 'row',
   },
   joinButton: {
     borderRadius: 8,
     flex: 1,
     justifyContent: 'center',
     marginHorizontal: 12,
     marginVertical: 3,
   },
   joinButtonContainer: {
     flexDirection: 'row',
     height: 48,
     marginBottom: 8,
   },
   editView: {
     marginLeft: 20,
     marginRight: 20,
     padding: 10,
     flexDirection: 'row',
     justifyContent: 'space-between',
   },
   editViewContent: {
     flex: 1,
     paddingRight: 6,
   },
   exitEditButton: {
     marginTop: 6,
   },
   editingLabel: {
     paddingBottom: 4,
   },
   editingMessagePreview: {
     color: 'listForegroundLabel',
   },
   joinButtonContent: {
     flexDirection: 'row',
     justifyContent: 'center',
     alignItems: 'center',
   },
   joinButtonTextLight: {
     color: 'white',
     fontSize: 20,
     marginHorizontal: 4,
   },
   joinButtonTextDark: {
     color: 'black',
     fontSize: 20,
     marginHorizontal: 4,
   },
   joinThreadLoadingIndicator: {
     paddingVertical: 2,
   },
   sendButton: {
     position: 'absolute',
     bottom: 4,
     left: 0,
   },
   sendIcon: {
     paddingLeft: 9,
     paddingRight: 8,
     paddingVertical: 6,
   },
   textInput: {
     backgroundColor: 'listInputBackground',
     borderRadius: 8,
     color: 'listForegroundLabel',
     fontSize: 16,
     marginLeft: 4,
     marginRight: 4,
     marginTop: 6,
     marginBottom: 8,
     maxHeight: 110,
     paddingHorizontal: 10,
     paddingVertical: 5,
   },
 };
 
 const joinThreadLoadingStatusSelector = createLoadingStatusSelector(
   joinThreadActionTypes,
 );
 const createThreadLoadingStatusSelector =
   createLoadingStatusSelector(newThreadActionTypes);
 
 type ConnectedChatInputBarBaseProps = {
   ...BaseProps,
   +onInputBarLayout?: (event: LayoutEvent) => mixed,
   +openCamera: () => mixed,
   +navigation?: ChatNavigationProp<'MessageList'>,
 };
 function ConnectedChatInputBarBase(props: ConnectedChatInputBarBaseProps) {
   const navContext = React.useContext(NavContext);
   const keyboardState = React.useContext(KeyboardContext);
   const inputState = React.useContext(InputStateContext);
   const overlayContext = React.useContext(OverlayContext);
 
   const viewerID = useSelector(
     state => state.currentUserInfo && state.currentUserInfo.id,
   );
   const draft = useSelector(
     state =>
       state.draftStore.drafts[draftKeyFromThreadID(props.threadInfo.id)] ?? '',
   );
   const joinThreadLoadingStatus = useSelector(joinThreadLoadingStatusSelector);
   const createThreadLoadingStatus = useSelector(
     createThreadLoadingStatusSelector,
   );
   const threadCreationInProgress = createThreadLoadingStatus === 'loading';
   const calendarQuery = useSelector(state =>
     nonThreadCalendarQuery({
       redux: state,
       navContext,
     }),
   );
   const nextLocalID = useNextLocalID();
   const userInfos = useSelector(state => state.userStore.userInfos);
 
   const styles = useStyles(unboundStyles);
   const colors = useColors();
 
   const isActive = React.useMemo(
     () => props.threadInfo.id === activeThreadSelector(navContext),
     [props.threadInfo.id, navContext],
   );
 
   const dispatch = useDispatch();
   const dispatchActionPromise = useDispatchActionPromise();
   const callJoinThread = useServerCall(joinThread);
 
   const userSearchIndex = useSelector(userStoreSearchIndex);
 
   const { parentThreadID } = props.threadInfo;
   const parentThreadInfo = useSelector(state =>
     parentThreadID ? threadInfoSelector(state)[parentThreadID] : null,
   );
 
   const mentionsCandidates = getMentionsCandidates(
     props.threadInfo,
     parentThreadInfo,
   );
 
   const chatMentionCandidates = useThreadChatMentionCandidates(
     props.threadInfo,
   );
 
   const messageEditingContext = React.useContext(MessageEditingContext);
 
   const editedMessageInfo = messageEditingContext?.editState.editedMessage;
   const editedMessagePreview = useMessagePreview(
     editedMessageInfo,
     props.threadInfo,
     getDefaultTextMessageRules(chatMentionCandidates).simpleMarkdownRules,
   );
   const editMessage = useEditMessage();
 
   return (
     <ChatInputBar
       {...props}
       viewerID={viewerID}
       draft={draft}
       joinThreadLoadingStatus={joinThreadLoadingStatus}
       threadCreationInProgress={threadCreationInProgress}
       calendarQuery={calendarQuery}
       nextLocalID={nextLocalID}
       userInfos={userInfos}
       colors={colors}
       styles={styles}
       isActive={isActive}
       keyboardState={keyboardState}
       dispatch={dispatch}
       dispatchActionPromise={dispatchActionPromise}
       joinThread={callJoinThread}
       inputState={inputState}
       userSearchIndex={userSearchIndex}
       mentionsCandidates={mentionsCandidates}
       parentThreadInfo={parentThreadInfo}
       editedMessagePreview={editedMessagePreview}
       editedMessageInfo={editedMessageInfo}
       editMessage={editMessage}
       navigation={props.navigation}
       overlayContext={overlayContext}
       messageEditingContext={messageEditingContext}
     />
   );
 }
 
 type DummyChatInputBarProps = {
   ...BaseProps,
   +onHeightMeasured: (height: number) => mixed,
 };
 const noop = () => {};
 function DummyChatInputBar(props: DummyChatInputBarProps): React.Node {
   const { onHeightMeasured, ...restProps } = props;
   const onInputBarLayout = React.useCallback(
     (event: LayoutEvent) => {
       const { height } = event.nativeEvent.layout;
       onHeightMeasured(height);
     },
     [onHeightMeasured],
   );
   return (
     <View pointerEvents="none">
       <ConnectedChatInputBarBase
         {...restProps}
         onInputBarLayout={onInputBarLayout}
         openCamera={noop}
       />
     </View>
   );
 }
 
 type ChatInputBarProps = {
   ...BaseProps,
   +navigation: ChatNavigationProp<'MessageList'>,
   +route: NavigationRoute<'MessageList'>,
 };
 const ConnectedChatInputBar: React.ComponentType<ChatInputBarProps> =
   React.memo<ChatInputBarProps>(function ConnectedChatInputBar(
     props: ChatInputBarProps,
   ) {
     const { navigation, route, ...restProps } = props;
     const keyboardState = React.useContext(KeyboardContext);
 
     const { threadInfo } = props;
     const imagePastedCallback = React.useCallback(
       imagePastedEvent => {
         if (threadInfo.id !== imagePastedEvent.threadID) {
           return;
         }
         const pastedImage: PhotoPaste = {
           step: 'photo_paste',
           dimensions: {
             height: imagePastedEvent.height,
             width: imagePastedEvent.width,
           },
           filename: imagePastedEvent.fileName,
           uri: 'file://' + imagePastedEvent.filePath,
           selectTime: 0,
           sendTime: 0,
           retries: 0,
         };
         navigation.navigate<'ImagePasteModal'>({
           name: ImagePasteModalRouteName,
           params: {
             imagePasteStagingInfo: pastedImage,
             thread: threadInfo,
           },
         });
       },
       [navigation, threadInfo],
     );
 
     React.useEffect(() => {
       const imagePasteListener = NativeAppEventEmitter.addListener(
         'imagePasted',
         imagePastedCallback,
       );
       return () => imagePasteListener.remove();
     }, [imagePastedCallback]);
 
     const chatContext = React.useContext(ChatContext);
     invariant(chatContext, 'should be set');
     const { setChatInputBarHeight, deleteChatInputBarHeight } = chatContext;
     const onInputBarLayout = React.useCallback(
       (event: LayoutEvent) => {
         const { height } = event.nativeEvent.layout;
         setChatInputBarHeight(threadInfo.id, height);
       },
       [threadInfo.id, setChatInputBarHeight],
     );
 
     React.useEffect(() => {
       return () => {
         deleteChatInputBarHeight(threadInfo.id);
       };
     }, [deleteChatInputBarHeight, threadInfo.id]);
 
     const openCamera = React.useCallback(() => {
       keyboardState?.dismissKeyboard();
       navigation.navigate<'ChatCameraModal'>({
         name: ChatCameraModalRouteName,
         params: {
           presentedFrom: route.key,
           thread: threadInfo,
         },
       });
     }, [keyboardState, navigation, route.key, threadInfo]);
 
     return (
       <ConnectedChatInputBarBase
         {...restProps}
         onInputBarLayout={onInputBarLayout}
         openCamera={openCamera}
         navigation={navigation}
       />
     );
   });
 
 export { ConnectedChatInputBar as ChatInputBar, DummyChatInputBar };
diff --git a/native/chat/typeahead-tooltip.react.js b/native/chat/typeahead-tooltip.react.js
index 612970955..d50d636cd 100644
--- a/native/chat/typeahead-tooltip.react.js
+++ b/native/chat/typeahead-tooltip.react.js
@@ -1,152 +1,152 @@
 // @flow
 
 import * as React from 'react';
 import { Platform, Text } from 'react-native';
 import { PanGestureHandler, FlatList } from 'react-native-gesture-handler';
 
 import {
   type TypeaheadMatchedStrings,
   type Selection,
   getNewTextAndSelection,
 } from 'lib/shared/mention-utils.js';
 import type { RelativeMemberInfo } from 'lib/types/thread-types.js';
 
 import UserAvatar from '../avatars/user-avatar.react.js';
 import Button from '../components/button.react.js';
 import { useStyles } from '../themes/colors.js';
 
 export type TypeaheadTooltipProps = {
   +text: string,
   +matchedStrings: TypeaheadMatchedStrings,
   +suggestedUsers: $ReadOnlyArray<RelativeMemberInfo>,
   +focusAndUpdateTextAndSelection: (text: string, selection: Selection) => void,
 };
 
 function TypeaheadTooltip(props: TypeaheadTooltipProps): React.Node {
   const {
     text,
     matchedStrings,
     suggestedUsers,
     focusAndUpdateTextAndSelection,
   } = props;
 
-  const { textBeforeAtSymbol, usernamePrefix } = matchedStrings;
+  const { textBeforeAtSymbol, query } = matchedStrings;
 
   const styles = useStyles(unboundStyles);
 
   const renderTypeaheadButton = React.useCallback(
     ({ item }: { item: RelativeMemberInfo, ... }) => {
       const onPress = () => {
         const { newText, newSelectionStart } = getNewTextAndSelection(
           textBeforeAtSymbol,
           text,
-          usernamePrefix,
+          query,
           item,
         );
 
         focusAndUpdateTextAndSelection(newText, {
           start: newSelectionStart,
           end: newSelectionStart,
         });
       };
 
       return (
         <Button onPress={onPress} style={styles.button} iosActiveOpacity={0.85}>
           <UserAvatar size="small" userID={item.id} />
           <Text style={styles.buttonLabel} numberOfLines={1}>
             @{item.username}
           </Text>
         </Button>
       );
     },
     [
       styles.button,
       styles.buttonLabel,
       textBeforeAtSymbol,
       text,
-      usernamePrefix,
+      query,
       focusAndUpdateTextAndSelection,
     ],
   );
 
   // This is a hack that was introduced due to a buggy behavior of a
   // absolutely positioned FlatList on Android.
 
   // There was a bug that was present when there were too few items in a
   // FlatList and it wasn't scrollable. It was only present on Android as
   // iOS has a default "bounce" animation, even if the list is too short.
   // The bug manifested itself when we tried to scroll the FlatList.
   // Because it was unscrollable we were really scrolling FlatList
   // below it (in the ChatList) as FlatList here has "position: absolute"
   // and is positioned over the other FlatList.
 
   // The hack here solves it by using a PanGestureHandler. This way Pan events
   // on TypeaheadTooltip FlatList are always caught by handler.
   // When the FlatList is scrollable it scrolls normally, because handler
   // passes those events down to it.
 
   // If it's not scrollable, the PanGestureHandler "swallows" them.
   // Normally it would trigger onGestureEvent callback, but we don't need to
   // handle those events. We just want them to be ignored
   // and that's what's actually happening.
 
   const flatList = React.useMemo(
     () => (
       <FlatList
         style={styles.container}
         contentContainerStyle={styles.contentContainer}
         data={suggestedUsers}
         renderItem={renderTypeaheadButton}
         keyboardShouldPersistTaps="always"
       />
     ),
     [
       renderTypeaheadButton,
       styles.container,
       styles.contentContainer,
       suggestedUsers,
     ],
   );
 
   const listWithConditionalHandler = React.useMemo(() => {
     if (Platform.OS === 'android') {
       return <PanGestureHandler>{flatList}</PanGestureHandler>;
     }
     return flatList;
   }, [flatList]);
 
   return listWithConditionalHandler;
 }
 
 const unboundStyles = {
   container: {
     position: 'absolute',
     maxHeight: 200,
     left: 0,
     right: 0,
     bottom: '100%',
     backgroundColor: 'typeaheadTooltipBackground',
     borderBottomWidth: 1,
     borderTopWidth: 1,
     borderColor: 'typeaheadTooltipBorder',
     borderStyle: 'solid',
   },
   contentContainer: {
     padding: 8,
   },
   button: {
     alignItems: 'center',
     flexDirection: 'row',
     innerHeight: 24,
     padding: 8,
     color: 'typeaheadTooltipText',
   },
   buttonLabel: {
     color: 'white',
     fontSize: 16,
     fontWeight: '400',
     marginLeft: 8,
   },
 };
 
 export default TypeaheadTooltip;
diff --git a/web/chat/chat-input-bar.react.js b/web/chat/chat-input-bar.react.js
index 2c09d8d2d..4ff59b5f9 100644
--- a/web/chat/chat-input-bar.react.js
+++ b/web/chat/chat-input-bar.react.js
@@ -1,661 +1,661 @@
 // @flow
 
 import invariant from 'invariant';
 import _difference from 'lodash/fp/difference.js';
 import * as React from 'react';
 
 import {
   joinThreadActionTypes,
   joinThread,
   newThreadActionTypes,
 } from 'lib/actions/thread-actions.js';
 import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
 import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
 import { threadInfoSelector } from 'lib/selectors/thread-selectors.js';
 import { userStoreSearchIndex } from 'lib/selectors/user-selectors.js';
 import {
   getTypeaheadUserSuggestions,
   getTypeaheadRegexMatches,
   getMentionsCandidates,
 } from 'lib/shared/mention-utils.js';
 import type { TypeaheadMatchedStrings } from 'lib/shared/mention-utils.js';
 import { localIDPrefix, trimMessage } from 'lib/shared/message-utils.js';
 import {
   threadHasPermission,
   viewerIsMember,
   threadFrozenDueToViewerBlock,
   threadActualMembers,
   checkIfDefaultMembersAreVoiced,
 } from 'lib/shared/thread-utils.js';
 import type { CalendarQuery } from 'lib/types/entry-types.js';
 import type { LoadingStatus } from 'lib/types/loading-types.js';
 import { messageTypes } from 'lib/types/message-types-enum.js';
 import { threadPermissions } from 'lib/types/thread-permission-types.js';
 import {
   type ThreadInfo,
   type ClientThreadJoinRequest,
   type ThreadJoinPayload,
   type RelativeMemberInfo,
 } from 'lib/types/thread-types.js';
 import { type UserInfos } from 'lib/types/user-types.js';
 import {
   type DispatchActionPromise,
   useServerCall,
   useDispatchActionPromise,
 } from 'lib/utils/action-utils.js';
 
 import css from './chat-input-bar.css';
 import TypeaheadTooltip from './typeahead-tooltip.react.js';
 import Button from '../components/button.react.js';
 import {
   type InputState,
   type PendingMultimediaUpload,
 } from '../input/input-state.js';
 import LoadingIndicator from '../loading-indicator.react.js';
 import { allowedMimeTypeString } from '../media/file-utils.js';
 import Multimedia from '../media/multimedia.react.js';
 import { useSelector } from '../redux/redux-utils.js';
 import { nonThreadCalendarQuery } from '../selectors/nav-selectors.js';
 import { webTypeaheadRegex } from '../utils/typeahead-utils.js';
 
 type BaseProps = {
   +threadInfo: ThreadInfo,
   +inputState: InputState,
 };
 type Props = {
   ...BaseProps,
   +viewerID: ?string,
   +joinThreadLoadingStatus: LoadingStatus,
   +threadCreationInProgress: boolean,
   +calendarQuery: () => CalendarQuery,
   +nextLocalID: number,
   +isThreadActive: boolean,
   +userInfos: UserInfos,
   +dispatchActionPromise: DispatchActionPromise,
   +joinThread: (request: ClientThreadJoinRequest) => Promise<ThreadJoinPayload>,
   +typeaheadMatchedStrings: ?TypeaheadMatchedStrings,
   +suggestedUsers: $ReadOnlyArray<RelativeMemberInfo>,
   +parentThreadInfo: ?ThreadInfo,
 };
 
 class ChatInputBar extends React.PureComponent<Props> {
   textarea: ?HTMLTextAreaElement;
   multimediaInput: ?HTMLInputElement;
 
   componentDidMount() {
     this.updateHeight();
     if (this.props.isThreadActive) {
       this.addReplyListener();
     }
   }
 
   componentWillUnmount() {
     if (this.props.isThreadActive) {
       this.removeReplyListener();
     }
   }
 
   componentDidUpdate(prevProps: Props) {
     if (this.props.isThreadActive && !prevProps.isThreadActive) {
       this.addReplyListener();
     } else if (!this.props.isThreadActive && prevProps.isThreadActive) {
       this.removeReplyListener();
     }
 
     const { inputState } = this.props;
     const prevInputState = prevProps.inputState;
     if (inputState.draft !== prevInputState.draft) {
       this.updateHeight();
     }
 
     if (
       inputState.draft !== prevInputState.draft ||
       inputState.textCursorPosition !== prevInputState.textCursorPosition
     ) {
       inputState.setTypeaheadState({
         canBeVisible: true,
       });
     }
 
     const curUploadIDs = ChatInputBar.unassignedUploadIDs(
       inputState.pendingUploads,
     );
     const prevUploadIDs = ChatInputBar.unassignedUploadIDs(
       prevInputState.pendingUploads,
     );
     if (
       this.multimediaInput &&
       _difference(prevUploadIDs)(curUploadIDs).length > 0
     ) {
       // Whenever a pending upload is removed, we reset the file
       // HTMLInputElement's value field, so that if the same upload occurs again
       // the onChange call doesn't get filtered
       this.multimediaInput.value = '';
     } else if (
       this.textarea &&
       _difference(curUploadIDs)(prevUploadIDs).length > 0
     ) {
       // Whenever a pending upload is added, we focus the textarea
       this.textarea.focus();
       return;
     }
 
     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',
       );
     }
   }
 
   static unassignedUploadIDs(
     pendingUploads: $ReadOnlyArray<PendingMultimediaUpload>,
   ) {
     return pendingUploads
       .filter(
         (pendingUpload: PendingMultimediaUpload) => !pendingUpload.messageID,
       )
       .map((pendingUpload: PendingMultimediaUpload) => pendingUpload.localID);
   }
 
   updateHeight() {
     const textarea = this.textarea;
     if (textarea) {
       textarea.style.height = 'auto';
       const newHeight = Math.min(textarea.scrollHeight, 150);
       textarea.style.height = `${newHeight}px`;
     }
   }
 
   addReplyListener() {
     invariant(
       this.props.inputState,
       'inputState should be set in addReplyListener',
     );
     this.props.inputState.addReplyListener(this.focusAndUpdateText);
   }
 
   removeReplyListener() {
     invariant(
       this.props.inputState,
       'inputState should be set in removeReplyListener',
     );
     this.props.inputState.removeReplyListener(this.focusAndUpdateText);
   }
 
   render() {
     const isMember = viewerIsMember(this.props.threadInfo);
     const canJoin = threadHasPermission(
       this.props.threadInfo,
       threadPermissions.JOIN_THREAD,
     );
 
     let joinButton = null;
     if (!isMember && canJoin && !this.props.threadCreationInProgress) {
       let buttonContent;
       if (this.props.joinThreadLoadingStatus === 'loading') {
         buttonContent = (
           <LoadingIndicator
             status={this.props.joinThreadLoadingStatus}
             size="medium"
             color="white"
           />
         );
       } else {
         buttonContent = (
           <>
             <SWMansionIcon icon="plus" size={24} />
             <p className={css.joinButtonText}>Join Chat</p>
           </>
         );
       }
       joinButton = (
         <div className={css.joinButtonContainer}>
           <Button
             variant="filled"
             buttonColor={{ backgroundColor: `#${this.props.threadInfo.color}` }}
             onClick={this.onClickJoin}
           >
             {buttonContent}
           </Button>
         </div>
       );
     }
 
     const { pendingUploads, cancelPendingUpload } = this.props.inputState;
     const multimediaPreviews = pendingUploads.map(pendingUpload => {
       const { uri, mediaType, thumbHash, dimensions } = pendingUpload;
       let mediaSource = { thumbHash, dimensions };
       if (mediaType !== 'encrypted_photo' && mediaType !== 'encrypted_video') {
         mediaSource = {
           ...mediaSource,
           type: mediaType,
           uri,
           thumbnailURI: null,
         };
       } else {
         const { encryptionKey } = pendingUpload;
         invariant(
           encryptionKey,
           'encryptionKey should be set for encrypted media',
         );
         mediaSource = {
           ...mediaSource,
           type: mediaType,
           blobURI: uri,
           encryptionKey,
           thumbnailBlobURI: null,
           thumbnailEncryptionKey: null,
         };
       }
       return (
         <Multimedia
           mediaSource={mediaSource}
           pendingUpload={pendingUpload}
           remove={cancelPendingUpload}
           multimediaCSSClass={css.multimedia}
           multimediaImageCSSClass={css.multimediaImage}
           key={pendingUpload.localID}
         />
       );
     });
     const previews =
       multimediaPreviews.length > 0 ? (
         <div className={css.previews}>{multimediaPreviews}</div>
       ) : null;
 
     let content;
     // If the thread is created by somebody else while the viewer is attempting
     // to create it, the threadInfo might be modified in-place and won't
     // list the viewer as a member, which will end up hiding the input. In
     // this case, we will assume that our creation action will get translated,
     // into a join and as long as members are voiced, we can show the input.
     const defaultMembersAreVoiced = checkIfDefaultMembersAreVoiced(
       this.props.threadInfo,
     );
 
     let sendButton;
     if (this.props.inputState.draft.length) {
       sendButton = (
         <a onClick={this.onSend} className={css.sendButton}>
           <SWMansionIcon
             icon="send-2"
             size={22}
             color={`#${this.props.threadInfo.color}`}
           />
         </a>
       );
     }
 
     if (
       threadHasPermission(this.props.threadInfo, threadPermissions.VOICED) ||
       (this.props.threadCreationInProgress && defaultMembersAreVoiced)
     ) {
       content = (
         <div className={css.inputBarWrapper}>
           <a className={css.multimediaUpload} onClick={this.onMultimediaClick}>
             <input
               type="file"
               onChange={this.onMultimediaFileChange}
               ref={this.multimediaInputRef}
               accept={allowedMimeTypeString}
               multiple
             />
             <SWMansionIcon
               icon="image-1"
               size={22}
               color={`#${this.props.threadInfo.color}`}
               disableFill
             />
           </a>
           <div className={css.inputBarTextInput}>
             <textarea
               rows="1"
               placeholder="Type your message"
               value={this.props.inputState.draft}
               onChange={this.onChangeMessageText}
               onKeyDown={this.onKeyDown}
               onClick={this.onClickTextarea}
               onSelect={this.onSelectTextarea}
               ref={this.textareaRef}
               autoFocus
             />
           </div>
           {sendButton}
         </div>
       );
     } else if (
       threadFrozenDueToViewerBlock(
         this.props.threadInfo,
         this.props.viewerID,
         this.props.userInfos,
       ) &&
       threadActualMembers(this.props.threadInfo.members).length === 2
     ) {
       content = (
         <span className={css.explanation}>
           You can&rsquo;t send messages to a user that you&rsquo;ve blocked.
         </span>
       );
     } else if (isMember) {
       content = (
         <span className={css.explanation}>
           You don&rsquo;t have permission to send messages.
         </span>
       );
     } else if (defaultMembersAreVoiced && canJoin) {
       content = null;
     } else {
       content = (
         <span className={css.explanation}>
           You don&rsquo;t have permission to send messages.
         </span>
       );
     }
 
     let typeaheadTooltip;
     if (
       this.props.inputState.typeaheadState.canBeVisible &&
       this.props.suggestedUsers.length > 0 &&
       this.props.typeaheadMatchedStrings &&
       this.textarea
     ) {
       typeaheadTooltip = (
         <TypeaheadTooltip
           inputState={this.props.inputState}
           textarea={this.textarea}
           matchedStrings={this.props.typeaheadMatchedStrings}
           suggestedUsers={this.props.suggestedUsers}
         />
       );
     }
 
     return (
       <div className={css.inputBar}>
         {joinButton}
         {previews}
         {content}
         {typeaheadTooltip}
       </div>
     );
   }
 
   textareaRef = (textarea: ?HTMLTextAreaElement) => {
     this.textarea = textarea;
     if (textarea) {
       textarea.focus();
     }
   };
 
   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) => {
     // We need to call focus() first on Safari, otherwise the cursor
     // ends up at the start instead of the end for some reason
     const { textarea } = this;
     invariant(textarea, 'textarea should be set');
     textarea.focus();
 
     // We reset the textarea to an empty string at the start so that the cursor
     // always ends up at the end, even if the text doesn't actually change
     textarea.value = '';
     const currentText = this.props.inputState.draft;
     if (!currentText.startsWith(text)) {
       const prependedText = text.concat(currentText);
       this.props.inputState.setDraft(prependedText);
       textarea.value = prependedText;
     } else {
       textarea.value = currentText;
     }
 
     // The above strategies make sure the cursor is at the end,
     // but we also need to make sure that we're scrolled to the bottom
     textarea.scrollTop = textarea.scrollHeight;
   };
 
   onKeyDown = (event: SyntheticKeyboardEvent<HTMLTextAreaElement>) => {
     const { accept, close, moveChoiceUp, moveChoiceDown } =
       this.props.inputState.typeaheadState;
 
     const actions = {
       Enter: accept,
       Tab: accept,
       ArrowDown: moveChoiceDown,
       ArrowUp: moveChoiceUp,
       Escape: close,
     };
 
     if (
       this.props.inputState.typeaheadState.canBeVisible &&
       actions[event.key]
     ) {
       event.preventDefault();
       actions[event.key]();
     } else if (event.key === 'Enter' && !event.shiftKey) {
       event.preventDefault();
       this.send();
     }
   };
 
   onSend = (event: SyntheticEvent<HTMLAnchorElement>) => {
     event.preventDefault();
     this.send();
   };
 
   send() {
     let { nextLocalID } = this.props;
 
     const text = trimMessage(this.props.inputState.draft);
     if (text) {
       this.dispatchTextMessageAction(text, nextLocalID);
       nextLocalID++;
     }
     if (this.props.inputState.pendingUploads.length > 0) {
       this.props.inputState.createMultimediaMessage(
         nextLocalID,
         this.props.threadInfo,
       );
     }
   }
 
   dispatchTextMessageAction(text: string, nextLocalID: number) {
     this.props.inputState.setDraft('');
 
     const localID = `${localIDPrefix}${nextLocalID}`;
     const creatorID = this.props.viewerID;
     invariant(creatorID, 'should have viewer ID in order to send a message');
 
     this.props.inputState.sendTextMessage(
       {
         type: messageTypes.TEXT,
         localID,
         threadID: this.props.threadInfo.id,
         text,
         creatorID,
         time: Date.now(),
       },
       this.props.threadInfo,
       this.props.parentThreadInfo,
     );
   }
 
   multimediaInputRef = (multimediaInput: ?HTMLInputElement) => {
     this.multimediaInput = multimediaInput;
   };
 
   onMultimediaClick = () => {
     if (this.multimediaInput) {
       this.multimediaInput.click();
     }
   };
 
   onMultimediaFileChange = async (
     event: SyntheticInputEvent<HTMLInputElement>,
   ) => {
     const result = await this.props.inputState.appendFiles([
       ...event.target.files,
     ]);
     if (!result && this.multimediaInput) {
       this.multimediaInput.value = '';
     }
   };
 
   onClickJoin = () => {
     this.props.dispatchActionPromise(joinThreadActionTypes, this.joinAction());
   };
 
   async joinAction() {
     const query = this.props.calendarQuery();
     return await this.props.joinThread({
       threadID: this.props.threadInfo.id,
       calendarQuery: {
         startDate: query.startDate,
         endDate: query.endDate,
         filters: [
           ...query.filters,
           { type: 'threads', threadIDs: [this.props.threadInfo.id] },
         ],
       },
     });
   }
 }
 
 const joinThreadLoadingStatusSelector = createLoadingStatusSelector(
   joinThreadActionTypes,
 );
 const createThreadLoadingStatusSelector =
   createLoadingStatusSelector(newThreadActionTypes);
 
 const ConnectedChatInputBar: React.ComponentType<BaseProps> =
   React.memo<BaseProps>(function ConnectedChatInputBar(props) {
     const viewerID = useSelector(
       state => state.currentUserInfo && state.currentUserInfo.id,
     );
     const nextLocalID = useSelector(state => state.nextLocalID);
     const isThreadActive = useSelector(
       state => props.threadInfo.id === state.navInfo.activeChatThreadID,
     );
     const userInfos = useSelector(state => state.userStore.userInfos);
     const joinThreadLoadingStatus = useSelector(
       joinThreadLoadingStatusSelector,
     );
     const createThreadLoadingStatus = useSelector(
       createThreadLoadingStatusSelector,
     );
     const threadCreationInProgress = createThreadLoadingStatus === 'loading';
     const calendarQuery = useSelector(nonThreadCalendarQuery);
     const dispatchActionPromise = useDispatchActionPromise();
     const callJoinThread = useServerCall(joinThread);
     const userSearchIndex = useSelector(userStoreSearchIndex);
 
     const { parentThreadID } = props.threadInfo;
     const parentThreadInfo = useSelector(state =>
       parentThreadID ? threadInfoSelector(state)[parentThreadID] : null,
     );
 
     const mentionsCandidates = getMentionsCandidates(
       props.threadInfo,
       parentThreadInfo,
     );
 
     const typeaheadRegexMatches = React.useMemo(
       () =>
         getTypeaheadRegexMatches(
           props.inputState.draft,
           {
             start: props.inputState.textCursorPosition,
             end: props.inputState.textCursorPosition,
           },
           webTypeaheadRegex,
         ),
       [props.inputState.textCursorPosition, props.inputState.draft],
     );
 
     const typeaheadMatchedStrings: ?TypeaheadMatchedStrings = React.useMemo(
       () =>
         typeaheadRegexMatches !== null
           ? {
               textBeforeAtSymbol:
                 typeaheadRegexMatches.groups?.textPrefix ?? '',
-              usernamePrefix: typeaheadRegexMatches.groups?.username ?? '',
+              query: typeaheadRegexMatches.groups?.username ?? '',
             }
           : null,
       [typeaheadRegexMatches],
     );
 
     React.useEffect(() => {
       if (props.inputState.typeaheadState.keepUpdatingThreadMembers) {
         const setter = props.inputState.setTypeaheadState;
         setter({
           frozenMentionsCandidates: mentionsCandidates,
         });
       }
     }, [
       mentionsCandidates,
       props.inputState.setTypeaheadState,
       props.inputState.typeaheadState.keepUpdatingThreadMembers,
     ]);
 
     const suggestedUsers: $ReadOnlyArray<RelativeMemberInfo> =
       React.useMemo(() => {
         if (!typeaheadMatchedStrings) {
           return [];
         }
         return getTypeaheadUserSuggestions(
           userSearchIndex,
           props.inputState.typeaheadState.frozenMentionsCandidates,
           viewerID,
-          typeaheadMatchedStrings.usernamePrefix,
+          typeaheadMatchedStrings.query,
         );
       }, [
         userSearchIndex,
         props.inputState.typeaheadState.frozenMentionsCandidates,
         viewerID,
         typeaheadMatchedStrings,
       ]);
 
     return (
       <ChatInputBar
         {...props}
         viewerID={viewerID}
         joinThreadLoadingStatus={joinThreadLoadingStatus}
         threadCreationInProgress={threadCreationInProgress}
         calendarQuery={calendarQuery}
         nextLocalID={nextLocalID}
         isThreadActive={isThreadActive}
         userInfos={userInfos}
         dispatchActionPromise={dispatchActionPromise}
         joinThread={callJoinThread}
         typeaheadMatchedStrings={typeaheadMatchedStrings}
         suggestedUsers={suggestedUsers}
         parentThreadInfo={parentThreadInfo}
       />
     );
   });
 
 export default ConnectedChatInputBar;
diff --git a/web/chat/typeahead-tooltip.react.js b/web/chat/typeahead-tooltip.react.js
index 2be4b6712..b8a958ca5 100644
--- a/web/chat/typeahead-tooltip.react.js
+++ b/web/chat/typeahead-tooltip.react.js
@@ -1,187 +1,187 @@
 // @flow
 
 import classNames from 'classnames';
 import * as React from 'react';
 
 import type { TypeaheadMatchedStrings } from 'lib/shared/mention-utils.js';
 import type { RelativeMemberInfo } from 'lib/types/thread-types.js';
 import { leastPositiveResidue } from 'lib/utils/math-utils.js';
 
 import css from './typeahead-tooltip.css';
 import type { InputState } from '../input/input-state.js';
 import {
   getTypeaheadOverlayScroll,
   getTypeaheadTooltipActions,
   getTypeaheadTooltipButtons,
   getTypeaheadTooltipPosition,
 } from '../utils/typeahead-utils.js';
 
 export type TypeaheadTooltipProps = {
   +inputState: InputState,
   +textarea: HTMLTextAreaElement,
   +matchedStrings: TypeaheadMatchedStrings,
   +suggestedUsers: $ReadOnlyArray<RelativeMemberInfo>,
 };
 
 function TypeaheadTooltip(props: TypeaheadTooltipProps): React.Node {
   const { inputState, textarea, matchedStrings, suggestedUsers } = props;
 
-  const { textBeforeAtSymbol, usernamePrefix } = matchedStrings;
+  const { textBeforeAtSymbol, query } = matchedStrings;
 
   const [isVisibleForAnimation, setIsVisibleForAnimation] =
     React.useState(false);
 
   const [chosenPositionInOverlay, setChosenPositionInOverlay] =
     React.useState<number>(0);
 
   const overlayRef = React.useRef<?HTMLDivElement>();
 
   React.useEffect(() => {
     setChosenPositionInOverlay(0);
   }, [suggestedUsers]);
 
   React.useEffect(() => {
     setIsVisibleForAnimation(true);
     const setter = inputState.setTypeaheadState;
     setter({
       keepUpdatingThreadMembers: false,
     });
 
     return () => {
       setter({
         keepUpdatingThreadMembers: true,
       });
       setIsVisibleForAnimation(false);
     };
   }, [inputState.setTypeaheadState]);
 
   const actions = React.useMemo(
     () =>
       getTypeaheadTooltipActions({
         inputStateDraft: inputState.draft,
         inputStateSetDraft: inputState.setDraft,
         inputStateSetTextCursorPosition: inputState.setTextCursorPosition,
         suggestedUsers,
         textBeforeAtSymbol,
-        usernamePrefix,
+        query,
       }),
     [
       inputState.draft,
       inputState.setDraft,
       inputState.setTextCursorPosition,
       suggestedUsers,
       textBeforeAtSymbol,
-      usernamePrefix,
+      query,
     ],
   );
 
   const tooltipPosition = React.useMemo(
     () =>
       getTypeaheadTooltipPosition(textarea, actions.length, textBeforeAtSymbol),
     [textarea, actions.length, textBeforeAtSymbol],
   );
 
   const tooltipPositionStyle = React.useMemo(
     () => ({
       top: tooltipPosition.top,
       left: tooltipPosition.left,
     }),
     [tooltipPosition],
   );
 
   const tooltipButtons = React.useMemo(
     () =>
       getTypeaheadTooltipButtons(
         setChosenPositionInOverlay,
         chosenPositionInOverlay,
         actions,
       ),
     [setChosenPositionInOverlay, actions, chosenPositionInOverlay],
   );
 
   const close = React.useCallback(() => {
     const setter = inputState.setTypeaheadState;
     setter({
       canBeVisible: false,
       moveChoiceUp: null,
       moveChoiceDown: null,
       close: null,
       accept: null,
     });
   }, [inputState.setTypeaheadState]);
 
   const accept = React.useCallback(() => {
     actions[chosenPositionInOverlay].execute();
     close();
   }, [actions, chosenPositionInOverlay, close]);
 
   const moveChoiceUp = React.useCallback(() => {
     if (actions.length === 0) {
       return;
     }
     setChosenPositionInOverlay(previousPosition =>
       leastPositiveResidue(previousPosition - 1, actions.length),
     );
   }, [setChosenPositionInOverlay, actions.length]);
 
   const moveChoiceDown = React.useCallback(() => {
     if (actions.length === 0) {
       return;
     }
     setChosenPositionInOverlay(previousPosition =>
       leastPositiveResidue(previousPosition + 1, actions.length),
     );
   }, [setChosenPositionInOverlay, actions.length]);
 
   React.useEffect(() => {
     const setter = inputState.setTypeaheadState;
     setter({
       canBeVisible: true,
       moveChoiceUp,
       moveChoiceDown,
       close,
       accept,
     });
 
     return close;
   }, [
     close,
     accept,
     moveChoiceUp,
     moveChoiceDown,
     actions,
     inputState.setTypeaheadState,
   ]);
 
   React.useEffect(() => {
     const current = overlayRef.current;
     if (current) {
       const newScrollTop = getTypeaheadOverlayScroll(
         current.scrollTop,
         chosenPositionInOverlay,
       );
       current.scrollTo(0, newScrollTop);
     }
   }, [chosenPositionInOverlay]);
 
   if (suggestedUsers.length === 0) {
     return null;
   }
 
   const overlayClasses = classNames(css.suggestionsContainer, {
     [css.notVisible]: !isVisibleForAnimation,
     [css.visible]: isVisibleForAnimation,
   });
 
   return (
     <div
       ref={overlayRef}
       className={overlayClasses}
       style={tooltipPositionStyle}
     >
       {tooltipButtons}
     </div>
   );
 }
 
 export default TypeaheadTooltip;
diff --git a/web/utils/typeahead-utils.js b/web/utils/typeahead-utils.js
index cc44faf4a..2e64e6414 100644
--- a/web/utils/typeahead-utils.js
+++ b/web/utils/typeahead-utils.js
@@ -1,215 +1,215 @@
 // @flow
 
 import classNames from 'classnames';
 import * as React from 'react';
 
 import { oldValidUsernameRegexString } from 'lib/shared/account-utils.js';
 import { getNewTextAndSelection } from 'lib/shared/mention-utils.js';
 import { stringForUserExplicit } from 'lib/shared/user-utils.js';
 import type { SetState } from 'lib/types/hook-types.js';
 import type { RelativeMemberInfo } from 'lib/types/thread-types.js';
 
 import UserAvatar from '../avatars/user-avatar.react.js';
 import { typeaheadStyle } from '../chat/chat-constants.js';
 import css from '../chat/typeahead-tooltip.css';
 import Button from '../components/button.react.js';
 
 const webTypeaheadRegex: RegExp = new RegExp(
   `(?<textPrefix>(?:^(?:.|\n)*\\s+)|^)@(?<username>${oldValidUsernameRegexString})?$`,
 );
 
 export type TypeaheadTooltipAction = {
   +key: string,
   +execute: () => mixed,
   +actionButtonContent: { +userID: string, +username: string },
 };
 
 export type TooltipPosition = {
   +top: number,
   +left: number,
 };
 
 function getCaretOffsets(
   textarea: HTMLTextAreaElement,
   text: string,
 ): { caretTopOffset: number, caretLeftOffset: number } {
   if (!textarea) {
     return { caretTopOffset: 0, caretLeftOffset: 0 };
   }
 
   // terribly hacky but it works I guess :D
   // we had to use it, as it's hard to count lines in textarea
   // and track cursor position within it as
   // lines can be wrapped into new lines without \n character
   // as result of overflow
   const textareaStyle: CSSStyleDeclaration = window.getComputedStyle(
     textarea,
     null,
   );
   const div = document.createElement('div');
 
   for (const styleName of textareaStyle) {
     div.style.setProperty(styleName, textareaStyle.getPropertyValue(styleName));
   }
 
   div.style.display = 'inline-block';
   div.style.position = 'absolute';
   div.textContent = text;
 
   const span = document.createElement('span');
   span.textContent = textarea.value.slice(text.length);
   div.appendChild(span);
 
   document.body?.appendChild(div);
   const { offsetTop, offsetLeft } = span;
   document.body?.removeChild(div);
 
   const textareaWidth = parseInt(textareaStyle.getPropertyValue('width'));
 
   const caretLeftOffset =
     offsetLeft + typeaheadStyle.tooltipWidth > textareaWidth
       ? textareaWidth - typeaheadStyle.tooltipWidth
       : offsetLeft;
 
   return {
     caretTopOffset: offsetTop - textarea.scrollTop,
     caretLeftOffset,
   };
 }
 export type GetTypeaheadTooltipActionsParams = {
   +inputStateDraft: string,
   +inputStateSetDraft: (draft: string) => mixed,
   +inputStateSetTextCursorPosition: (newPosition: number) => mixed,
   +suggestedUsers: $ReadOnlyArray<RelativeMemberInfo>,
   +textBeforeAtSymbol: string,
-  +usernamePrefix: string,
+  +query: string,
 };
 
 function getTypeaheadTooltipActions(
   params: GetTypeaheadTooltipActionsParams,
 ): $ReadOnlyArray<TypeaheadTooltipAction> {
   const {
     inputStateDraft,
     inputStateSetDraft,
     inputStateSetTextCursorPosition,
     suggestedUsers,
     textBeforeAtSymbol,
-    usernamePrefix,
+    query,
   } = params;
   return suggestedUsers
     .filter(
       suggestedUser => stringForUserExplicit(suggestedUser) !== 'anonymous',
     )
     .map(suggestedUser => ({
       key: suggestedUser.id,
       execute: () => {
         const { newText, newSelectionStart } = getNewTextAndSelection(
           textBeforeAtSymbol,
           inputStateDraft,
-          usernamePrefix,
+          query,
           suggestedUser,
         );
 
         inputStateSetDraft(newText);
         inputStateSetTextCursorPosition(newSelectionStart);
       },
       actionButtonContent: {
         userID: suggestedUser.id,
         username: stringForUserExplicit(suggestedUser),
       },
     }));
 }
 
 function getTypeaheadTooltipButtons(
   setChosenPositionInOverlay: SetState<number>,
   chosenPositionInOverlay: number,
   actions: $ReadOnlyArray<TypeaheadTooltipAction>,
 ): $ReadOnlyArray<React.Node> {
   return actions.map((action, idx) => {
     const { key, execute, actionButtonContent } = action;
     const buttonClasses = classNames(css.suggestion, {
       [css.suggestionHover]: idx === chosenPositionInOverlay,
     });
 
     const onMouseMove: (
       event: SyntheticEvent<HTMLButtonElement>,
     ) => mixed = () => {
       setChosenPositionInOverlay(idx);
     };
 
     return (
       <Button
         key={key}
         onClick={execute}
         onMouseMove={onMouseMove}
         className={buttonClasses}
       >
         <UserAvatar size="small" userID={actionButtonContent.userID} />
         <span className={css.username}>@{actionButtonContent.username}</span>
       </Button>
     );
   });
 }
 
 function getTypeaheadOverlayScroll(
   currentScrollTop: number,
   chosenActionPosition: number,
 ): number {
   const upperButtonBoundary = chosenActionPosition * typeaheadStyle.rowHeight;
   const lowerButtonBoundary =
     (chosenActionPosition + 1) * typeaheadStyle.rowHeight;
 
   if (upperButtonBoundary < currentScrollTop) {
     return upperButtonBoundary;
   } else if (
     lowerButtonBoundary - typeaheadStyle.tooltipMaxHeight >
     currentScrollTop
   ) {
     return (
       lowerButtonBoundary +
       typeaheadStyle.tooltipVerticalPadding -
       typeaheadStyle.tooltipMaxHeight
     );
   }
 
   return currentScrollTop;
 }
 
 function getTypeaheadTooltipPosition(
   textarea: HTMLTextAreaElement,
   actionsLength: number,
   textBeforeAtSymbol: string,
 ): TooltipPosition {
   const { caretTopOffset, caretLeftOffset } = getCaretOffsets(
     textarea,
     textBeforeAtSymbol,
   );
 
   const textareaBoundingClientRect = textarea.getBoundingClientRect();
 
   const top: number =
     textareaBoundingClientRect.top -
     Math.min(
       typeaheadStyle.tooltipVerticalPadding +
         actionsLength * typeaheadStyle.rowHeight,
       typeaheadStyle.tooltipMaxHeight,
     ) -
     typeaheadStyle.tooltipTopOffset +
     caretTopOffset;
 
   const left: number =
     textareaBoundingClientRect.left -
     typeaheadStyle.tooltipLeftOffset +
     caretLeftOffset;
 
   return { top, left };
 }
 
 export {
   webTypeaheadRegex,
   getCaretOffsets,
   getTypeaheadTooltipActions,
   getTypeaheadTooltipButtons,
   getTypeaheadOverlayScroll,
   getTypeaheadTooltipPosition,
 };