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 UI component shows it. Use this page to see how a message is put together, change how its text renders, and replace parts of a message or the whole message UI. The props are in the Message and MessageUI references.

How a message is rendered

For each message, the list renders <Message/>, which:

  1. creates the message's handlers: react, delete, flag, mark unread, mute, pin, open the thread, and retry a failed send;
  2. puts them, with the message, in MessageContext;
  3. renders the message UI: the component registered under the MessageUI key, by default the SDK's <MessageUI/>.

The message UI gets no props. It reads the message and its handlers with useMessageContext(), and so does any component it renders.

Parts of the default message UI

The default <MessageUI/> is made of parts:

Two default messages with their parts outlined and numbered: 1, Cara's avatar; 2, the message text; 3, a file attachment; 4, a quoted message in the user's reply; 5, the reactions; 6, the read status; 7, the actions menu; 8, the time and Edited mark; 9, the 2 replies button; 10, the Pinned by You mark

Cara's message is pinned, and has a file attachment, reactions and replies. The user's own message below it quotes Cara's, was edited and has been read, and its actions show because the pointer is over it. Each numbered part can be replaced on its own, through its <WithComponents/> key:

Part Component to override (<WithComponents/> key)
1. The sender's avatar Avatar
2. The text MessageText, which renders the text with renderText
3. Attachments, including a shared location Attachment
4. A quoted message QuotedMessage
5. Reactions MessageReactions
6. Status: sending, sent, delivered or read MessageStatus
7. The actions menu MessageActions
8. The timestamp and the edited mark MessageTimestamp, MessageEditedIndicator
9. The replies button, in a channel MessageRepliesCountButton
10. Pinned, reminder, translated and also-sent-in-channel marks PinIndicator, ReminderNotification, MessageTranslationIndicator, MessageAlsoSentInChannelIndicator
A deleted or blocked message MessageDeleted (no default), MessageDeletedBubble, MessageBlocked

A deleted or blocked message replaces the message UI's content:

  • A deleted message shows "Message deleted" in place of its bubble, without actions: MessageDeletedBubble. A message counts as deleted when it has deleted_at, its type is 'deleted', or the user deleted it only for themselves (deleted_for_me). A MessageDeleted component of your own replaces the whole message instead; it has no default.
  • A blocked message shows "Message was blocked by moderation policies", with no avatar, time or status: MessageBlocked. A message is blocked when it's shadowed, or when its type is 'error' and moderation removed it (moderation.action is 'remove').
A deleted message from Cara, a bubble reading Message deleted with a no-entry icon, between Bob's message and Cara's next one
A blocked message of the user's own, a pill reading Message was blocked by moderation policies, between Bob's message and Cara's

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 UI.
  • An AI-generated message, as <Chat/>'s isMessageAIGenerated decides, renders its text with <StreamedMessageText/>, which shows the text as it streams in.

Bubble content

The bubble shows <MessageBubbleContent/>, which renders defaultMessageBubbleContentSet. Each item shows only when the message object has the corresponding property, in this order:

Item (type) Shows when Renders
quotedMessage The message quotes another one: message.quoted_message The QuotedMessage key
poll The message has a poll: message.poll_id <Poll/>
attachments The message has attachments or a shared location The Attachment key, with the shared location at the top.
text The message has text, or isMessageAIGenerated marks it as AI generated The MessageText key, or the StreamedMessageText key for an AI-generated message

The default <StreamedMessageText/> renders the text it has streamed so far through the MessageText key, so a MessageText override also applies to an AI-generated message.

A poll message also shows its text and attachments. While a message has a poll, its root element has the str-chat__message--has-poll class. A single photo without text normally fills the bubble edge to edge, through the str-chat__message--has-single-attachment class. With a poll in the bubble, that class isn't added, so the photo keeps the bubble's padding, like the poll.

MessageBubbleContent props lists the fields of an item.

To reorder, remove or add content, register a component under the MessageBubbleContent key that renders <MessageBubbleContent messageBubbleContentSet={yourSet} />. Here is a TextFirstMessages component whose messages show their text above the poll and attachments:

src/TextFirstMessages.tsx
import {
  defaultMessageBubbleContentSet,
  MessageBubbleContent,
  MessageList,
  WithComponents,
} from "stream-chat-react";
import type { MessageBubbleContentSetItem } from "stream-chat-react";

const order = ["quotedMessage", "text", "poll", "attachments"];

// Built once, so every message gets the same set.
const textFirstContentSet: MessageBubbleContentSetItem[] = order.flatMap(
  (type) => defaultMessageBubbleContentSet.filter((item) => item.type === type),
);

const TextFirstBubbleContent = () => (
  <MessageBubbleContent messageBubbleContentSet={textFirstContentSet} />
);

const overrides = { MessageBubbleContent: TextFirstBubbleContent };

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

renderText

renderText turns a message's text into what the message UI shows. The default renders Markdown with react-markdown, and adds mentions and emoji. To change how text renders, pass your own function to the list as its renderText prop; both <MessageList/> and <VirtualizedMessageList/> take it.

It's called as renderText(text, options). The mentions come in options.messageMentionEntities.

Here is a renderTextWithoutHeadings function that keeps the default and renders Markdown headings as bold text:

src/ChannelMessages.tsx
import type { RenderTextFunction } from "stream-chat-react";
import { MessageList, renderText } from "stream-chat-react";

const renderTextWithoutHeadings: RenderTextFunction = (text, options) =>
  renderText(text, {
    ...options,
    customMarkDownRenderers: {
      ...options?.customMarkDownRenderers,
      h1: ({ children }) => <strong>{children}</strong>,
      h2: ({ children }) => <strong>{children}</strong>,
    },
  });

export const ChannelMessages = () => (
  <MessageList renderText={renderTextWithoutHeadings} />
);

Keep the options the list passes in: they carry the mentions to render. The options are in the renderText reference.

Replacing parts

Register replacements with <WithComponents/>. Overriding components explains how overrides apply.

Component to override (<WithComponents/> key) Replaces
MessageUI A whole message. Gets no props; reads useMessageContext().
VirtualMessage A whole message in <VirtualizedMessageList/>. Without it, the list uses MessageUI.
The keys in Parts of the default message UI One part of a message
MessageSystem A system message
MessageBubbleContent What a message's bubble holds, as in Bubble content

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

src/CompactMessages.tsx
import {
  MessageList,
  MessageText,
  MessageTimestamp,
  useMessageContext,
  WithComponents,
} from "stream-chat-react";

const CompactMessage = () => {
  const { message } = useMessageContext();

  return (
    <div className="compact-message">
      <MessageTimestamp />
      <strong>{message.user?.name ?? message.user?.id}</strong>
      <MessageText />
    </div>
  );
};

const overrides = { MessageUI: CompactMessage };

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

<MessageText/> and <MessageTimestamp/> read the message from MessageContext too, so they need no props. The Custom message recipe builds a complete message UI.

Used in