Message UI

Each message in a message list is rendered in two steps: Message prepares the message and what can be done with it, and a message view, MessageItemView by default, shows it. Use this page to see how a message is put together, to change what its bubble holds and how its text renders, and to replace parts of a message or the whole message view. The props are in the Message and Message UI references, and the parts and their props are on Message UI components.

How a message is rendered

For each message, the list renders a row: the date separator when a day starts, the message, and the unread separator after the last read message. A system message renders MessageSystem instead of a message. For every other message the list renders Message, which:

  1. creates the message's handlers: react, reply, edit, delete, flag, mute, pin, open the thread, and retry a failed send;
  2. puts them in MessageContext, with the message and onLongPress, which opens the message menu;
  3. renders the MessageItemView key, by default the SDK's MessageItemView.

The message view gets no props. It reads the message and its handlers with useMessageContext(), and so does any component it renders. The context also holds messageActionHandlers, the handlers behind the message's actions, which the message menu reads; see Message actions in your own components.

A message that moderation blocked skips the message view: Message renders MessageBlocked in its place, with the message as a prop.

Parts of the default message view

The default MessageItemView is made of parts, each its own override key:

Part Override key
The sender's avatar, on other users' messages, once for each group of messages MessageAuthor
The pinned, reminder, saved-for-later and also-sent-in-channel marks above the message MessageHeader, with MessagePinnedHeader, MessageReminderHeader, MessageSavedForLaterHeader, SentToChannelHeader
The bubble with its content MessageContent, and the content items
A quoted message Reply
Images and videos, files, other attachments Gallery, FileAttachmentGroup, Attachment
The text of an AI reply as it streams in StreamingMessageView
Reactions, above or below the bubble ReactionListTop, ReactionListBottom
The replies button, in a channel MessageReplies, with MessageRepliesAvatars
The sender's name, the status, the timestamp and the "Edited" mark MessageFooter, with MessageStatus and MessageTimestamp
The error badge on a message that failed MessageError
A deleted message MessageDeleted
A blocked message MessageBlocked
A message bounced by moderation MessageBounce

MessageSpacer has no default. When you set it, it renders last in each message row, outside the bubble. MessageContentTopView, MessageContentBottomView, MessageContentLeadingView and MessageContentTrailingView have no default either: they render around the content, inside the bubble.

MessageFooter shows, from start to end, the sender's name on other users' messages in a group channel, the status mark on the user's own messages, the time, and "Edited" on an edited message. A message that is not the last of its group has no footer, unless it is still sending or failed. The status mark is one of:

  • a clock, while the message is sending;
  • a check, once the server has it;
  • a double check, once another member received it;
  • a double check in a color of its own, once another member read it.

MessageStatus shows only the sending mark while the channel's read events are turned off, because the other marks come from other members' read and delivery events. It shows nothing on a message that failed, on an error message, or on a deleted one. The status shows on the user's own messages, including the parent message at the top of a thread when the user sent it. showMessageStatus on Message turns it on or off for a message.

Deleted, blocked and failed messages

  • A deleted message shows "Message deleted" in place of its bubble: MessageDeleted, without actions. It keeps the footer, with the time, and no status mark. A message counts as deleted when its type is 'deleted'.
  • A blocked message shows its text in a pill, with no avatar, time or status: MessageBlocked. A message is blocked when its type is 'error' and moderation removed it (moderation.action is 'remove').
  • A message that failed to send shows an error badge at the edge of the bubble, MessageError. Pressing the message opens the message menu, so the user can retry or delete it. A message the server bounced opens MessageBounce on a press or a long press, which asks the user to send it anyway, edit it or delete it.

Some messages render differently:

  • A poll renders inside the message's bubble, with the message's text and attachments, as in Bubble content.
  • A system message, such as "Cleo joined the channel", renders the MessageSystem component instead of a message.
  • An AI-generated message, as the isMessageAIGenerated prop of Chat decides, renders its text with StreamingMessageView, which shows the text as it streams in.

Bubble content

The bubble holds the message's content, in the order messageContentOrder on Channel and Thread gives. Each item renders only when the message has the corresponding property. The default order is:

Item (MessageContentType) Shows when Renders
quoted_reply The message quotes another one, and none of its attachments has actions The Reply key. Pressing it goes to the original message.
gallery The message has images or videos The Gallery key
files The message has files The FileAttachmentGroup key
poll The message has a poll: message.poll_id The poll
ai_text isMessageAIGenerated marks the message as AI generated The StreamingMessageView key
attachments The message has attachments other than images, videos and files The Attachment key, once for each attachment
location The message has a shared location: message.shared_location The MessageLocation key. It has no default, so nothing without it.
text The message has text, and isn't AI generated The text of the message

An item left out of messageContentOrder doesn't render. A message with none of its items, and that isn't deleted, renders nothing.

Here is a TextFirstMessages component whose messages show their text above the attachments, and no location:

src/TextFirstMessages.tsx
import type { Channel as StreamChannel } from "stream-chat";
import {
  Channel,
  MessageComposer,
  MessageList,
} from "stream-chat-react-native";
import type { MessageContentType } from "stream-chat-react-native";

// Declared once, so every render passes the same array.
const messageContentOrder: MessageContentType[] = [
  "quoted_reply",
  "text",
  "poll",
  "gallery",
  "files",
  "attachments",
  "ai_text",
];

export const TextFirstMessages = ({ channel }: { channel: StreamChannel }) => (
  <Channel channel={channel} messageContentOrder={messageContentOrder}>
    <MessageList />
    <MessageComposer />
  </Channel>
);

Thread takes the same prop, so pass it to both.

The text

The text of a message renders as Markdown, translated into the user's language when the message has a translation for it. Three things change it:

  • markdownRules on Channel and Thread replaces the rules that turn Markdown into components.
  • messageTextNumberOfLines limits how many lines of text a message shows.
  • The MessageText key replaces the rendering of the text. It has no default. It gets the props of the text container, and renderText, the function the default uses, so it can render the default with changes.

The theme's messageItemView.content.markdown key styles the Markdown. See Theming.

Pressing a message

Pressing and long pressing a message go through two handlers of Message. onLongPress opens the message menu, except for a blocked message, a message with attachment actions, or when enableLongPress is false. onPress dismisses the keyboard when dismissKeyboardOnMessageTouch is on, and opens the menu for a message that failed. onPressMessage and onLongPressMessage on Channel and Thread receive each press with a defaultHandler, so they add to the default or replace it. A press on a message that failed doesn't reach onPressMessage. The message menu is on Overlays and bottom sheets.

Swiping a message toward the start quotes it: enableSwipeToReply, on by default, turns this on and off, and customMessageSwipeAction replaces what the swipe does. MessageSwipeContent is the icon that shows while swiping.

Replacing parts

Register replacements with WithComponents. Overriding components explains how overrides apply.

Override key Replaces
MessageItemView A message's look. Gets no props; reads useMessageContext().
Message A whole message, its handlers included. Gets the message and its place in the list as props.
The keys in Parts of the default message view One part of a message
MessageSystem A system message

Here is a CompactMessages list that shows each message as one compact line:

src/CompactMessages.tsx
import { Pressable, StyleSheet, Text, View } from "react-native";
import {
  MessageList,
  MessageTextContainer,
  useMessageContext,
  WithComponents,
} from "stream-chat-react-native";

const CompactMessage = () => {
  const { contextMenuAnchorRef, message, onLongPress } = useMessageContext();

  return (
    // The default view opens the message menu on a long press; a view of your own does it too.
    <Pressable
      onLongPress={(event) => onLongPress({ emitter: "messageContent", event })}
    >
      <View ref={contextMenuAnchorRef} style={styles.row}>
        <Text style={styles.name}>
          {message.user?.name ?? message.user?.id}
        </Text>
        <MessageTextContainer />
      </View>
    </Pressable>
  );
};

const overrides = { MessageItemView: CompactMessage };

export const CompactMessages = () => (
  <WithComponents overrides={overrides}>
    <MessageList />
  </WithComponents>
);

const styles = StyleSheet.create({
  name: { fontWeight: "600" },
  row: {
    flexDirection: "row",
    gap: 8,
    paddingHorizontal: 16,
    paddingVertical: 4,
  },
});

MessageTextContainer reads the message from MessageContext too, so it needs no props. The message menu opens from the onLongPress in the context, which Message created. It positions itself on the view in contextMenuAnchorRef. The Custom message UI cookbook builds a complete message view.

Used in