diff --git a/lib/shared/mention-utils.js b/lib/shared/mention-utils.js
--- a/lib/shared/mention-utils.js
+++ b/lib/shared/mention-utils.js
@@ -23,6 +23,19 @@
   +end: number,
 };
 
+type TypeaheadUserSuggestionItem = {
+  +type: 'user',
+  +userInfo: RelativeMemberInfo,
+};
+
+export type TypeaheadSuggestionItem = TypeaheadUserSuggestionItem;
+
+export type TypeaheadTooltipActionItem<SuggestionItemType> = {
+  +key: string,
+  +execute: () => void,
+  +actionButtonContent: SuggestionItemType,
+};
+
 // 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(
@@ -69,7 +82,7 @@
   threadMembers: $ReadOnlyArray<RelativeMemberInfo>,
   viewerID: ?string,
   usernamePrefix: string,
-): $ReadOnlyArray<RelativeMemberInfo> {
+): $ReadOnlyArray<TypeaheadUserSuggestionItem> {
   const userIDs = userSearchIndex.getSearchResults(usernamePrefix);
   const usersInThread = threadOtherMembers(threadMembers, viewerID);
 
@@ -77,26 +90,25 @@
     .filter(user => usernamePrefix.length === 0 || userIDs.includes(user.id))
     .sort((userA, userB) =>
       stringForUserExplicit(userA).localeCompare(stringForUserExplicit(userB)),
-    );
+    )
+    .map(userInfo => ({ type: 'user', userInfo }));
 }
 
 function getNewTextAndSelection(
   textBeforeAtSymbol: string,
   entireText: string,
-  usernamePrefix: string,
-  user: RelativeMemberInfo,
+  textPrefix: string,
+  suggestionText: string,
 ): {
   newText: string,
   newSelectionStart: number,
 } {
-  const totalMatchLength =
-    textBeforeAtSymbol.length + usernamePrefix.length + 1; // 1 for @ char
+  const totalMatchLength = textBeforeAtSymbol.length + textPrefix.length + 1; // 1 for @ char
 
   let newSuffixText = entireText.slice(totalMatchLength);
   newSuffixText = (newSuffixText[0] !== ' ' ? ' ' : '') + newSuffixText;
 
-  const newText =
-    textBeforeAtSymbol + '@' + stringForUserExplicit(user) + newSuffixText;
+  const newText = textBeforeAtSymbol + suggestionText + newSuffixText;
 
   const newSelectionStart = newText.length - newSuffixText.length + 1;
 
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
@@ -129,7 +129,11 @@
 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';
+import {
+  nativeTypeaheadRegex,
+  mentionTypeaheadTooltipActions,
+  mentionTypeaheadTooltipButtonRenderer,
+} from '../utils/typeahead-utils.js';
 
 /* eslint-disable import/no-named-as-default-member */
 const { Value, Clock, block, set, cond, neq, sub, interpolateNode, stopClock } =
@@ -567,8 +571,10 @@
           <TypeaheadTooltip
             text={this.state.text}
             matchedStrings={typeaheadMatchedStrings}
-            suggestedUsers={suggestedUsers}
+            suggestions={suggestedUsers}
             focusAndUpdateTextAndSelection={this.focusAndUpdateTextAndSelection}
+            typeaheadButtonRenderer={mentionTypeaheadTooltipButtonRenderer}
+            typeaheadTooltipActionsGetter={mentionTypeaheadTooltipActions}
           />
         );
       }
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
@@ -1,67 +1,64 @@
 // @flow
 
 import * as React from 'react';
-import { Platform, Text } from 'react-native';
+import { Platform } from 'react-native';
 import { PanGestureHandler, FlatList } from 'react-native-gesture-handler';
 
 import {
   type TypeaheadMatchedStrings,
   type Selection,
-  getNewTextAndSelection,
+  type TypeaheadTooltipActionItem,
 } 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';
+import type {
+  TypeaheadTooltipActionsParams,
+  TypeaheadTooltipButtonRendererParams,
+} from '../utils/typeahead-utils.js';
 
-export type TypeaheadTooltipProps = {
+export type TypeaheadTooltipStyles = typeof unboundStyles;
+
+export type TypeaheadTooltipProps<SuggestionItemType> = {
   +text: string,
   +matchedStrings: TypeaheadMatchedStrings,
-  +suggestedUsers: $ReadOnlyArray<RelativeMemberInfo>,
+  +suggestions: $ReadOnlyArray<SuggestionItemType>,
   +focusAndUpdateTextAndSelection: (text: string, selection: Selection) => void,
+  +typeaheadButtonRenderer: (
+    TypeaheadTooltipButtonRendererParams<SuggestionItemType>,
+  ) => React.Node,
+  +typeaheadTooltipActionsGetter: (
+    TypeaheadTooltipActionsParams<SuggestionItemType>,
+  ) => $ReadOnlyArray<TypeaheadTooltipActionItem<SuggestionItemType>>,
 };
 
-function TypeaheadTooltip(props: TypeaheadTooltipProps): React.Node {
+function TypeaheadTooltip<SuggestionItemType>(
+  props: TypeaheadTooltipProps<SuggestionItemType>,
+): React.Node {
   const {
     text,
     matchedStrings,
-    suggestedUsers,
+    suggestions,
     focusAndUpdateTextAndSelection,
+    typeaheadButtonRenderer,
+    typeaheadTooltipActionsGetter,
   } = props;
 
   const { textBeforeAtSymbol, textPrefix } = matchedStrings;
 
   const styles = useStyles(unboundStyles);
-
-  const renderTypeaheadButton = React.useCallback(
-    ({ item }: { item: RelativeMemberInfo, ... }) => {
-      const onPress = () => {
-        const { newText, newSelectionStart } = getNewTextAndSelection(
-          textBeforeAtSymbol,
-          text,
-          textPrefix,
-          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>
-      );
-    },
+  const actions = React.useMemo(
+    () =>
+      typeaheadTooltipActionsGetter({
+        suggestions,
+        textBeforeAtSymbol,
+        text,
+        textPrefix,
+        focusAndUpdateTextAndSelection,
+      }),
     [
-      styles.button,
-      styles.buttonLabel,
+      typeaheadTooltipActionsGetter,
+      suggestions,
       textBeforeAtSymbol,
       text,
       textPrefix,
@@ -69,6 +66,29 @@
     ],
   );
 
+  const renderTypeaheadButton = React.useCallback(
+    ({
+      item,
+    }: {
+      item: TypeaheadTooltipActionItem<SuggestionItemType>,
+      ...
+    }) => {
+      return (
+        <Button
+          onPress={item.execute}
+          style={styles.button}
+          iosActiveOpacity={0.85}
+        >
+          {typeaheadButtonRenderer({
+            item,
+            styles,
+          })}
+        </Button>
+      );
+    },
+    [styles, typeaheadButtonRenderer],
+  );
+
   // This is a hack that was introduced due to a buggy behavior of a
   // absolutely positioned FlatList on Android.
 
@@ -95,17 +115,12 @@
       <FlatList
         style={styles.container}
         contentContainerStyle={styles.contentContainer}
-        data={suggestedUsers}
+        data={actions}
         renderItem={renderTypeaheadButton}
         keyboardShouldPersistTaps="always"
       />
     ),
-    [
-      renderTypeaheadButton,
-      styles.container,
-      styles.contentContainer,
-      suggestedUsers,
-    ],
+    [actions, renderTypeaheadButton, styles.container, styles.contentContainer],
   );
 
   const listWithConditionalHandler = React.useMemo(() => {
diff --git a/native/utils/typeahead-utils.js b/native/utils/typeahead-utils.js
--- a/native/utils/typeahead-utils.js
+++ b/native/utils/typeahead-utils.js
@@ -1,6 +1,19 @@
 // @flow
 
+import * as React from 'react';
+import { Text } from 'react-native';
+
 import { oldValidUsernameRegexString } from 'lib/shared/account-utils.js';
+import {
+  getNewTextAndSelection,
+  type Selection,
+  type TypeaheadTooltipActionItem,
+  type TypeaheadSuggestionItem,
+} from 'lib/shared/mention-utils.js';
+import { stringForUserExplicit } from 'lib/shared/user-utils.js';
+
+import UserAvatar from '../avatars/user-avatar.react.js';
+import type { TypeaheadTooltipStyles } from '../chat/typeahead-tooltip.react.js';
 
 // Native regex is a little bit different than web one as
 // there are no named capturing groups yet on native.
@@ -8,4 +21,83 @@
   `((^(.|\n)*\\s+)|^)@(${oldValidUsernameRegexString})?$`,
 );
 
-export { nativeTypeaheadRegex };
+export type TypeaheadTooltipActionsParams<SuggestionType> = {
+  +suggestions: $ReadOnlyArray<SuggestionType>,
+  +textBeforeAtSymbol: string,
+  +text: string,
+  +textPrefix: string,
+  +focusAndUpdateTextAndSelection: (text: string, selection: Selection) => void,
+};
+function mentionTypeaheadTooltipActions({
+  suggestions,
+  textBeforeAtSymbol,
+  text,
+  textPrefix,
+  focusAndUpdateTextAndSelection,
+}: TypeaheadTooltipActionsParams<TypeaheadSuggestionItem>): $ReadOnlyArray<
+  TypeaheadTooltipActionItem<TypeaheadSuggestionItem>,
+> {
+  const actions = [];
+  for (const suggestion of suggestions) {
+    if (suggestion.type === 'user') {
+      const { userInfo } = suggestion;
+      const resolvedUsername = stringForUserExplicit(userInfo);
+      if (resolvedUsername === 'anonymous') {
+        continue;
+      }
+      const mentionText = `@${resolvedUsername}`;
+      actions.push({
+        key: userInfo.id,
+        execute: () => {
+          const { newText, newSelectionStart } = getNewTextAndSelection(
+            textBeforeAtSymbol,
+            text,
+            textPrefix,
+            mentionText,
+          );
+          focusAndUpdateTextAndSelection(newText, {
+            start: newSelectionStart,
+            end: newSelectionStart,
+          });
+        },
+        actionButtonContent: {
+          type: 'user',
+          userInfo,
+        },
+      });
+    }
+  }
+  return actions;
+}
+
+export type TypeaheadTooltipButtonRendererParams<SuggestionItemType> = {
+  +item: TypeaheadTooltipActionItem<SuggestionItemType>,
+  +styles: TypeaheadTooltipStyles,
+};
+function mentionTypeaheadTooltipButtonRenderer({
+  item,
+  styles,
+}: TypeaheadTooltipButtonRendererParams<TypeaheadSuggestionItem>): React.Node {
+  let avatarComponent = null;
+  let typeaheadTooltipButtonText = null;
+  if (item.actionButtonContent.type === 'user') {
+    avatarComponent = (
+      <UserAvatar size="small" userID={item.actionButtonContent.userInfo.id} />
+    );
+    typeaheadTooltipButtonText = item.actionButtonContent.userInfo.username;
+  }
+  return (
+    <>
+      {avatarComponent}
+      <Text style={styles.buttonLabel} numberOfLines={1}>
+        {typeaheadTooltipButtonText}
+      </Text>
+    </>
+  );
+}
+
+export {
+  nativeTypeaheadRegex,
+  mentionTypeaheadTooltipActions,
+  mentionTypeaheadTooltipButtonRenderer,
+};
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
@@ -631,7 +631,7 @@
           props.inputState.typeaheadState.frozenMentionsCandidates,
           viewerID,
           typeaheadMatchedStrings.textPrefix,
-        );
+        ).map(suggestion => suggestion.userInfo);
       }, [
         userSearchIndex,
         props.inputState.typeaheadState.frozenMentionsCandidates,
diff --git a/web/utils/typeahead-utils.js b/web/utils/typeahead-utils.js
--- a/web/utils/typeahead-utils.js
+++ b/web/utils/typeahead-utils.js
@@ -107,7 +107,7 @@
           textBeforeAtSymbol,
           inputStateDraft,
           textPrefix,
-          suggestedUser,
+          stringForUserExplicit(suggestedUser),
         );
 
         inputStateSetDraft(newText);