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

Message list UI

The message list is the scrolling part of a channel or a thread. It renders what a message paginator holds and turns the user's scrolling into paginator calls. This page covers the two lists, what they show after a jump, the separators and floating controls, and how one message is put together. The props are in the MessageList and MessageFlashList references.

MessageList and MessageFlashList

Both lists render the messages of the channel or thread they are in:

  • Inside a Thread, they render the thread's replies, thread.messagePaginator, with the parent message above them.
  • Otherwise they render the channel's messages, channel.messagePaginator.

So the same <MessageList /> works in both places. Each list subscribes to its paginator's state, and:

  • loads older messages when the user scrolls within 100 points of the oldest loaded message;
  • loads newer messages when the user scrolls within 100 points of the newest loaded message of an interval that isn't the latest one;
  • tells the paginator whether the user is viewing the latest messages: the app is in the foreground, the latest messages are loaded, and the newest one is on screen. While that holds, a new message doesn't count as unread, and in a channel the list marks the channel read when one arrives. Read state, unread and receipts covers the rules.

A load in one direction waits for a load in flight in the other, so the two never shift the scroll position under each other. How the paginator holds intervals of messages is on Message paginators.

The two lists differ in what they're built on:

MessageList MessageFlashList
Built on React Native's FlatList, inverted FlashList from @shopify/flash-list, which you install
Status Stable Experimental: its implementation can change
Extra list props additionalFlatListProps additionalFlashListProps
Prop for the older end FooterComponent HeaderComponent
Prop for the newer end HeaderComponent FooterComponent
A list shorter than the screen Loads older pages until the messages fill the screen Loads older pages as the user scrolls up
Cells Created for each message Recycled between messages of the same shape, such as text only or with a gallery

Use MessageList unless profiling shows the list is the bottleneck. MessageFlashList throws when @shopify/flash-list isn't installed. The list props are named after the list's own ends: MessageList is inverted, so its footer is at the top of the screen.

Jumps on screen

A jump moves the list to one message: a quoted message the user taps, a search result, a pinned message, the first unread message or the latest one. The paginator loads the interval of messages around the target, and the list shows it:

  1. The interval replaces what was on screen, and once the target is rendered, the list scrolls it to the middle.
  2. The target is highlighted. The highlight lasts 3 seconds, DEFAULT_HIGHLIGHT_DURATION, counted from when the list scrolled to it.
  3. If the interval isn't the latest one, the scroll-to-bottom button shows, and scrolling down loads newer messages.

A jump is a paginator call, so you start one the same way from anywhere: MessagePaginator.jumpToMessage(messageId, { focusSignalTtlMs: DEFAULT_HIGHLIGHT_DURATION }) on channel.messagePaginator, or on thread.messagePaginator for a reply. The list reacts to the paginator's messageFocusSignal, even when the jump started before the list mounted. The jump itself is on Message paginators, and the places that start one are on Moving between channels, threads and messages.

Floating controls

Over the list, three elements tell the user where they are:

  • The unread notification, UnreadMessagesNotification, sits at the top of a channel's list. It shows "5 unread" while every message on screen is newer than the last message the user read, so that message is above the screen. Tapping it jumps to the first unread message, and its close button marks the channel read. It doesn't show in a thread, in a channel without read events (unless the channel counts unread messages locally), or while the attachment picker shows the photo grid.
  • The scroll-to-bottom button, ScrollToBottomButton, shows while the list isn't at the latest message, or while the latest messages aren't loaded. In a channel, its badge shows the user's unread count from the channel's read state; in a thread, it shows no badge. Tapping it scrolls to the newest message, or jumps to the latest messages when they aren't loaded. In a channel it then marks the channel read, and the unread separator stays where it was.
  • The sticky date header, StickyHeader, sits at the top and shows the date of the topmost message on screen. It hides when the oldest message of the channel is on screen, where the inline date separator says the same. hideStickyDateHeader turns it off.

NetworkDownIndicator shows "Waiting for network..." while the device is offline, and "Reconnecting..." while the connection to Stream is down.

The list also hosts two things that float over it but belong to other parts. The composer's suggestion list renders here, just above the composer, so a screen with a MessageComposer and no message list shows no suggestions. The NotificationList of the channel or thread renders here too; see Displaying notifications.

Separators

Date separators mark where a day starts. An InlineDateSeparator goes above the first message of each day. System messages, such as "Cleo joined the channel", get one only with allowDateSeparatorForSystemMessages. hideDateSeparators turns them off. The separator's date format, and the sticky header's, are translation keys; Localization covers them.

The unread separator, InlineUnreadIndicator, marks where the unread messages start, with "5 new messages". It renders under the last message the user read, from the paginator's unread snapshot: the unread state when the user opened the channel. Opening the channel marks it read but keeps the snapshot, so the user can still see where the new messages start. The snapshot clears when the user closes the unread notification, and when a new message arrives while the user is viewing the latest messages. shouldShowUnreadUnderlay={false} on Channel turns the separator off. Read state and unread counts explains the snapshot.

Messages from the same user, sent one after another, are grouped: the sender's avatar and the timestamp show on the last message of the group. A message that is still sending or that failed keeps its footer, so its status stays visible. enableMessageGroupingByUser={false} on Channel, or noGroupByUser on the list, turns grouping off, and maxTimeBetweenGroupedMessages starts a new group after a gap.

Loading, empty and error states

State What the list renders Override key
Loading the first page A spinner with "Loading messages..." in place of the list MessageListLoadingIndicator
Loading older messages A spinner at the older end. In a thread, under the parent message. None (the list's prop)
Loading newer messages A spinner at the newer end None (the list's prop)
A channel with no messages EmptyStateIndicator with listType="message". A thread with no replies shows its parent message instead. EmptyStateIndicator
The first page failed Channel or Thread renders LoadingErrorIndicator in place of its children LoadingErrorIndicator

A failed load of older or newer messages is recorded as lastQueryError in the paginator's state, and the list stays as it is. The next scroll to that end tries again.

A message

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.

The message is Message. It creates the message's handlers (react, reply, edit, delete, flag, mute, pin, open the thread, retry a failed send), puts them in MessageContext with the message, along with onLongPress, which opens the message menu. Then it renders the MessageItemView override, by default MessageItemView, with no props. A message view reads everything from useMessageContext().

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

Part Override key
The pinned, reminder, saved-for-later and also-sent-in-channel marks above the message MessageHeader, with MessagePinnedHeader, MessageReminderHeader, MessageSavedForLaterHeader, SentToChannelHeader
The sender's avatar, on other users' messages MessageAuthor
The bubble with its content MessageContent
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 timestamp, the "Edited" mark and the status MessageFooter, with MessageTimestamp and MessageStatus
The replies button, in a channel MessageReplies
A deleted or failed message MessageDeleted, MessageError

A poll renders in the bubble. messageContentOrder on Channel sets the order of the content inside the bubble. The text renders Markdown; markdownRules on Channel changes the rules, and the Message reference covers the parts in detail.

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 A message One part of a message
MessageSystem A system message
InlineDateSeparator The date separators between messages
StickyHeader, DateHeader The sticky date header, and the date label it renders
InlineUnreadIndicator The unread separator
UnreadMessagesNotification The unread notification
ScrollToBottomButton The scroll-to-bottom button. Gets onPress and showNotification.
TypingIndicator, TypingIndicatorContainer The typing indicator and its container
NetworkDownIndicator The connection banner
EmptyStateIndicator, MessageListLoadingIndicator The empty and first-page loading states

The spinners at the two ends are props of the list, not override keys: pass FooterComponent and HeaderComponent to MessageList, or HeaderComponent and FooterComponent to MessageFlashList. In a thread, the default at the older end is ThreadFooterComponent, which renders the parent message; a replacement there replaces the parent message too. At the newer end, the default also renders the typing indicator, so a replacement there removes it unless it renders one itself.

This list 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