Info:
This is release candidate documentation for Stream Chat React Native SDK v10. For the latest stable version, see the latest version (v9).

Custom suggestions

The composer suggests items while the user types a trigger:

Trigger Suggests Example
@ people and groups @tom, @channel, @here, @admin, @Support Team
/ commands /giphy
: emoji, once you add it :pizza

This UI cookbook changes the suggestion list's header and rows, and adds a list for a trigger of your own. Where the suggestions come from, and how to add a trigger, is explained in Suggestions. Emoji suggestions need the emoji middleware: see the Emoji suggestions UI cookbook.

Enabling mention types

User mentions always work. @channel, @here, roles and user groups appear only for users with the matching capability, and roles and user groups also need data on the server. Mentions lists the capabilities and where to grant them. The mentions middleware checks them itself, so a row you replace never shows a mention the user can't make.

Best practices

  • Replace the smallest part that does the job: AutoCompleteSuggestionHeader for the header, MentionSuggestionItem for the @ rows, AutoCompleteSuggestionItem for every row, and AutoCompleteSuggestionList only for a trigger of your own or a list that looks different.
  • Fall back to the SDK's component for the triggers and mention types you don't change, so they keep their behavior.
  • Register the overrides with WithComponents around the screens they apply to. Channels and threads both read them.
  • Pick an item with TextComposer.handleSelect(item). It runs the trigger's middleware, which writes the item into the text and closes the list.
  • Set keyboardShouldPersistTaps="always" on a list of your own, so the first tap on a row picks it rather than closing the keyboard.
  • Keep rows light. The list renders again as the user types.

What the list renders

MessageList and MessageFlashList render AutoCompleteSuggestionList above the composer. It follows the text composer's suggestions and the open search source's items, and renders nothing while there are no items. It renders:

  • AutoCompleteSuggestionHeader above the rows, with queryText and triggerType. By default, the commands header reads "Instant Commands", the emoji header Emoji matching "<query>", and mentions have no header.
  • AutoCompleteSuggestionItem for each row, with itemProps, the suggestion, and triggerType. It's a button that picks the item, around the row for the trigger: MentionSuggestionItem for @, CommandSuggestionItem for / and EmojiSuggestionItem for :.

triggerType is "mention", "command" or "emoji". The list knows these three triggers only, and renders nothing for any other, even when the trigger's middleware opened suggestions. See Rows for a trigger of your own.

Change the header

The default headers aren't translation keys. This header translates them with keys of your own, adds one for mentions, and keeps the default for emoji:

src/SuggestionHeader.tsx
import { StyleSheet, Text } from "react-native";
import {
  asDynamicKey,
  AutoCompleteSuggestionHeader,
  useTheme,
  useTranslationContext,
} from "stream-chat-react-native";
import type { AutoCompleteSuggestionHeaderProps } from "stream-chat-react-native";

export const SuggestionHeader = (props: AutoCompleteSuggestionHeaderProps) => {
  const { t } = useTranslationContext();
  const {
    theme: { semantics },
  } = useTheme();

  const title =
    props.triggerType === "command"
      ? t(asDynamicKey("myApp.suggestions.commands.title"), "Commands")
      : props.triggerType === "mention"
        ? t(asDynamicKey("myApp.suggestions.mentions.title"), "Mention someone")
        : null;

  if (!title) return <AutoCompleteSuggestionHeader {...props} />;

  return (
    <Text
      accessibilityRole="header"
      style={[styles.title, { color: semantics.textSecondary }]}
    >
      {title}
    </Text>
  );
};

const styles = StyleSheet.create({
  title: { fontSize: 13, fontWeight: "600", padding: 12 },
});
A suggestion list with the header Mention someone and the channel and here rows on iOS
A suggestion list with the header Mention someone and the channel and here rows on Android

asDynamicKey marks a key that isn't in the SDK's catalog, and the second argument is the English copy. Localization explains app keys.

Change the rows

AutoCompleteSuggestionItem renders every row, so a replacement handles the press too. This one renders commands on one line, as /giphy [text], and keeps the default rows for mentions and emoji:

src/CompactCommandItem.tsx
import { Pressable, StyleSheet, Text } from "react-native";
import type { CommandSuggestion, TextComposerSuggestion } from "stream-chat";
import {
  AutoCompleteSuggestionItem,
  useMessageComposer,
  useTheme,
} from "stream-chat-react-native";
import type { AutoCompleteSuggestionItemProps } from "stream-chat-react-native";

const isCommand = (item: TextComposerSuggestion): item is CommandSuggestion =>
  "name" in item && "args" in item;

export const CompactCommandItem = (props: AutoCompleteSuggestionItemProps) => {
  const { itemProps, triggerType } = props;
  const { textComposer } = useMessageComposer();
  const {
    theme: { semantics },
  } = useTheme();

  if (triggerType !== "command" || !isCommand(itemProps)) {
    return <AutoCompleteSuggestionItem {...props} />;
  }

  return (
    <Pressable
      accessibilityRole="button"
      onPress={() => void textComposer.handleSelect(itemProps)}
      style={styles.row}
    >
      <Text style={[styles.name, { color: semantics.textPrimary }]}>
        /{itemProps.name}
      </Text>
      <Text style={{ color: semantics.textTertiary }}>{itemProps.args}</Text>
    </Pressable>
  );
};

const styles = StyleSheet.create({
  name: { fontWeight: "600" },
  row: {
    flexDirection: "row",
    gap: 8,
    paddingHorizontal: 16,
    paddingVertical: 10,
  },
});
A suggestion list with the header Commands and one-line command rows on iOS
A suggestion list with the header Commands and one-line command rows on Android

TextComposer.handleSelect() sets the command, as picking it from the default row does. The default row also grays out a command that can't be used in this message, such as a moderation command in a reply that quotes a message. This row doesn't. Picking such a command shows a warning and doesn't set it, from this row or the default one. MessageComposer.isCommandDisabled(command) tells whether a command is disabled.

Rows per mention type

MentionSuggestionItem renders the @ rows inside the default button, so replacing it changes the row and keeps the press. It receives the suggestion as its props, and mentionType tells which kind it is:

mentionType Default row Shows
user MentionUserItem The avatar and the name, with the typed part in bold
channel / here MentionBroadcastItem @channel or @here and a description
role MentionRoleItem The role and Notify all {{ role }} members
user_group MentionUserGroupItem The group's name and description

The SDK exports the parts the default rows are built from: MentionItem, the row layout with a leading slot; EnhancedMentionIcon, an icon in a circle; EnhancedMentionContent, a title and a subtitle; and TokenizedSuggestionParts, a name with the typed part in bold. This one changes the @channel and @here rows and keeps the others:

src/BroadcastMentionRow.tsx
import type { MentionSuggestion } from "stream-chat";
import {
  EnhancedMentionContent,
  EnhancedMentionIcon,
  MentionItem,
  MentionSuggestionItem,
  useComponentsContext,
} from "stream-chat-react-native";

export const BroadcastMentionRow = (item: MentionSuggestion) => {
  const { icons } = useComponentsContext();

  if (item.mentionType !== "channel" && item.mentionType !== "here") {
    return <MentionSuggestionItem {...item} />;
  }

  return (
    <MentionItem leading={<EnhancedMentionIcon Icon={icons.Megaphone} />}>
      <EnhancedMentionContent
        subtitle={
          item.mentionType === "channel"
            ? "Everyone in this channel gets a notification"
            : "Everyone online right now gets a notification"
        }
        title={`@${item.mentionType}`}
      />
    </MentionItem>
  );
};

The parts take their styles from messageComposer.suggestions.mention in the theme, so the row above follows them too. MentionItem applies container and column, EnhancedMentionIcon applies enhancedMentionIcon, and EnhancedMentionContent applies enhancedMentionContainer, enhancedMentionTitle and enhancedMentionSubtitle. The theme also has name, tag and avatarSize under mention, but no row reads them. See Theming.

Rows for a trigger of your own

Custom triggers adds a middleware that suggests tags after #, each with the tag as its id. The default list renders nothing for #, so replace the list. This one renders the tags, and the default list for every other trigger:

src/SuggestionListWithTags.tsx
import { FlatList, Pressable, StyleSheet, Text, View } from "react-native";
import type {
  SearchSourceState,
  TextComposerState,
  TextComposerSuggestion,
} from "stream-chat";
import {
  AutoCompleteSuggestionList,
  useMessageComposer,
  useStateStore,
  useTheme,
} from "stream-chat-react-native";

const suggestionsSelector = ({ suggestions }: TextComposerState) => ({
  suggestions,
});
const itemsSelector = ({
  items,
}: SearchSourceState<TextComposerSuggestion>) => ({
  items,
});

export const SuggestionListWithTags = () => {
  const { textComposer } = useMessageComposer();
  const { suggestions } = useStateStore(
    textComposer.state,
    suggestionsSelector,
  );
  const { items } =
    useStateStore(suggestions?.searchSource.state, itemsSelector) ?? {};
  const {
    theme: { semantics },
  } = useTheme();

  if (suggestions?.trigger !== "#") return <AutoCompleteSuggestionList />;
  if (!items?.length) return null;

  return (
    <View
      style={[
        styles.list,
        {
          backgroundColor: semantics.backgroundCoreElevation1,
          borderColor: semantics.borderCoreDefault,
        },
      ]}
    >
      <FlatList
        data={items}
        keyboardShouldPersistTaps="always"
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <Pressable
            accessibilityRole="button"
            onPress={() => void textComposer.handleSelect(item)}
            style={styles.row}
          >
            <Text style={{ color: semantics.textPrimary }}>#{item.id}</Text>
          </Pressable>
        )}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  list: { borderTopWidth: 1, maxHeight: 240 },
  row: { paddingHorizontal: 16, paddingVertical: 10 },
});

The list subscribes to two stores, as the default one does: the text composer's state, for which trigger is open, and the open search source's state, for its items. Both selectors are declared at module scope. Picking a tag runs the middleware's onSuggestionItemSelect, which writes #design and a space into the text.

Register the overrides

Put the replacements in one object, declared once, and pass it to WithComponents:

src/ChannelScreen.tsx
import type { Channel as StreamChannel } from "stream-chat";
import {
  Channel,
  MessageComposer,
  MessageList,
  WithComponents,
} from "stream-chat-react-native";
import type { ComponentOverrides } from "stream-chat-react-native";
import { BroadcastMentionRow } from "./BroadcastMentionRow";
import { CompactCommandItem } from "./CompactCommandItem";
import { SuggestionHeader } from "./SuggestionHeader";
import { SuggestionListWithTags } from "./SuggestionListWithTags";

const overrides: ComponentOverrides = {
  AutoCompleteSuggestionHeader: SuggestionHeader,
  AutoCompleteSuggestionItem: CompactCommandItem,
  AutoCompleteSuggestionList: SuggestionListWithTags,
  MentionSuggestionItem: BroadcastMentionRow,
};

export const ChannelScreen = ({ channel }: { channel: StreamChannel }) => (
  <WithComponents overrides={overrides}>
    <Channel channel={channel}>
      <MessageList />
      <MessageComposer />
    </Channel>
  </WithComponents>
);

The default list reads the header and the row from the overrides, so SuggestionListWithTags shows SuggestionHeader and CompactCommandItem for the built-in triggers. A Thread on its own screen needs the same WithComponents around it.

Hide the suggestions

AutoCompleteSuggestionList: () => null hides the list for every trigger. The middleware still runs and still searches, so mention suggestions keep querying members as the user types. To stop a trigger, remove its middleware in the composer setup function instead, as Hide commands in the UI does for commands. The mentions middleware's id is stream-io/text-composer/mentions-middleware.

Custom composers

A composer layout of your own keeps the suggestions as long as it renders InputView or AutoCompleteInput, because the text input updates the text composer that opens them. The list itself renders in the message list, so a screen without MessageList or MessageFlashList shows none. The Custom composer UI cookbook builds such a layout.