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:
- creates the message's handlers: react, reply, edit, delete, flag, mute, pin, open the thread, and retry a failed send;
- puts them in
MessageContext, with the message andonLongPress, which opens the message menu; - renders the
MessageItemViewkey, by default the SDK'sMessageItemView.
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.
The footer and the status
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 itstypeis'deleted'. - A blocked message shows its text in a pill, with no avatar, time or status:
MessageBlocked. A message is blocked when itstypeis'error'and moderation removed it (moderation.actionis'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 opensMessageBounceon 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
MessageSystemcomponent instead of a message. - An AI-generated message, as the
isMessageAIGeneratedprop ofChatdecides, renders its text withStreamingMessageView, 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:
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:
markdownRulesonChannelandThreadreplaces the rules that turn Markdown into components.messageTextNumberOfLineslimits how many lines of text a message shows.- The
MessageTextkey replaces the rendering of the text. It has no default. It gets the props of the text container, andrenderText, 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:
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.