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

Livestream message list

A livestream chat is a busy channel with a large audience: many messages a second, from people who mostly read, next to a video. This UI cookbook builds that screen. It turns off the events the audience doesn't need, keeps an unread count on the device, limits how often and how much the message list renders, and lays the chat out under or over the video. The settings themselves are explained in Performance.

Best practices

  • Turn read, typing and connect events off for the channel type, on the server. An event that isn't sent costs nothing.
  • Keep read events off, and count unread messages on the device with localUnreadCountEnabled if the UI needs a count.
  • Cap the loaded messages with maxLoadedItems, so a long stream doesn't keep every message in memory.
  • Raise stateThrottleMs for livestream channels only, and check that the chat still feels live.
  • Use MessageFlashList with isLiveStreaming, and measure in a release build on a real device.
  • Leave out what a stream audience doesn't use, such as threads.

The channel type

Each channel type has settings on the server that apply to all its channels. For a livestream, turn off these in the Stream Dashboard:

  • Read events. Read state for thousands of viewers is costly, and no one looks at who read a message in a stream.
  • Typing events. With many people typing at once, a typing indicator says nothing.
  • Connect events. The presence events sent when members connect and disconnect.

Without read events, Channel doesn't mark the channel read and the list shows no unread notification, unless the device counts unread messages itself, as below. Channel settings lists what each setting costs, and Slow mode limits how often each viewer can send.

The client settings

The rest is set on the client, once, right after you create it:

src/configureLivestreams.ts
import type { StreamChat } from "stream-chat";

export const configureLivestreams = (client: StreamChat) => {
  // Count unread messages on the device in channels without read events.
  client.config.set({
    channel: { readEvents: { localUnreadCountEnabled: true } },
  });

  // Livestream channels only: render less often, and keep fewer messages.
  client.config.setSetupFunction("channel", ({ channel }) => {
    if (channel.type !== "livestream") return;
    channel.messagePaginator.updateConfig({
      maxLoadedItems: 300,
      stateThrottleMs: 1000,
    });
  });
};
  • localUnreadCountEnabled makes the client count new messages in channels without read events. The count lives in the user's own read state, so the scroll-to-bottom button shows it in its badge and the unread notification shows. When the SDK would mark the channel read, it calls Channel.markReadLocally() instead, which resets the count and sends nothing. It's off by default, and channels with read events on ignore it. Turning read events off covers it.
  • stateThrottleMs sets how often the message list publishes new messages and reactions from other users: at most once every 500 ms by default, here once a second. The user's own sends and reactions, loading a page and jumping publish at once. Other people's messages appear up to a second later, in batches.
  • maxLoadedItems caps the messages the list keeps. Once the newest messages hold more than the cap, the oldest are dropped as new ones arrive, and scrolling back loads them again. The lists pause the cap while the user reads near the oldest loaded message. It's off by default.

readEvents is a channel setting, so it applies to every channel; a channel has no updateConfig(). The paginator settings go through the channel setup function instead, which runs for every channel the client has and every one it creates later, so they apply to livestream channels only. livestream is the name of Stream's built-in livestream type; use the name of yours. Throttling and the memory cap explains both paginator settings, and Configuration the setup functions.

The message list

MessageFlashList renders the messages with FlashList from @shopify/flash-list, which you install. It reuses the views of rows that scroll out for the rows that scroll in, where MessageList creates a view for every message. isLiveStreaming tunes either list for many messages a second: it renders the messages at most once per frame, and reports scrolling every frame. Both are experimental.

src/LivestreamChat.tsx
import { useCallback } from "react";
import type { ReactNode } from "react";
import { StyleSheet, View } from "react-native";
import type { Channel as StreamChannel } from "stream-chat";
import {
  Channel,
  MessageComposer,
  MessageFlashList,
  messageActions as defaultMessageActions,
} from "stream-chat-react-native";
import type { MessageActionsProp } from "stream-chat-react-native";

export const LivestreamChat = ({
  channel,
  video,
}: {
  channel: StreamChannel;
  video: ReactNode;
}) => {
  // The stream has no threads, so the menu has no Thread Reply.
  const messageActions = useCallback<MessageActionsProp>(
    (params) =>
      defaultMessageActions(params).filter(
        (action) => action.actionType !== "threadReply",
      ),
    [],
  );

  return (
    <View style={styles.screen}>
      <View style={styles.video}>{video}</View>
      <View style={styles.chat}>
        <Channel channel={channel} messageActions={messageActions}>
          <MessageFlashList isLiveStreaming />
          <MessageComposer />
        </Channel>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  chat: { flex: 2 },
  screen: { flex: 1 },
  video: { aspectRatio: 16 / 9, backgroundColor: "black" },
});
A black video area above a chat list and composer on iOS
A black video area above a chat list and composer on Android

Pass your video player as video. The chat takes the space under it.

  • The list is given no onThreadSelect, so the replies button and Thread Reply would do nothing. The function removes Thread Reply from the message menu. To stop replies altogether, turn them off for the channel type on the server.
  • Channel marks the channel read, here locally, when it opens with unread messages, and the list does so again whenever a new message arrives while the user is at the newest message.
  • MessageFlashList throws when @shopify/flash-list isn't installed. MessageList works the same way here, without it: with isLiveStreaming, it also scrolls to a new message while the user is within 300 points of the newest one, instead of 10.
  • When the message menu closes, the message animates back into the list, and can pass over the video above it. To keep the video drawn above it, wrap the video's view in PortalWhileClosingView, as Fixed UI above a closing menu explains.

MessageList and MessageFlashList lists the differences between the two lists.

Chat over the video

A common layout puts the chat over the video, in the lower part of the screen. The list's background is the messageList.container style of the theme, so a transparent one lets the video show through. Give the bubbles a background of their own, so the messages stay readable over any frame. A ThemeProvider around the list applies both to this list only:

src/OverlayLivestreamChat.tsx
import type { PropsWithChildren } from "react";
import { StyleSheet, View } from "react-native";
import type { Channel as StreamChannel } from "stream-chat";
import {
  Channel,
  MessageComposer,
  MessageFlashList,
  ThemeProvider,
  useTheme,
} from "stream-chat-react-native";
import type { DeepPartial, Theme } from "stream-chat-react-native";

// Declared once, so the theme isn't merged again on every render.
const overVideo: DeepPartial<Theme> = {
  messageList: { container: { backgroundColor: "transparent" } },
  semantics: {
    chatBgIncoming: "rgba(0, 0, 0, 0.55)",
    chatBgOutgoing: "rgba(0, 0, 0, 0.7)",
    chatTextIncoming: "#FFFFFF",
    chatTextOutgoing: "#FFFFFF",
  },
};

const OverVideoTheme = ({ children }: PropsWithChildren) => {
  const { theme } = useTheme();

  return (
    <ThemeProvider style={overVideo} theme={theme}>
      {children}
    </ThemeProvider>
  );
};

export const OverlayLivestreamChat = ({
  channel,
  children,
}: PropsWithChildren<{ channel: StreamChannel }>) => (
  <View style={styles.screen}>
    {/* The video player, filling the screen. */}
    <View style={StyleSheet.absoluteFill}>{children}</View>
    <View style={styles.chat}>
      <Channel channel={channel}>
        <OverVideoTheme>
          <MessageFlashList isLiveStreaming />
        </OverVideoTheme>
        <MessageComposer />
      </Channel>
    </View>
  </View>
);

const styles = StyleSheet.create({
  chat: { bottom: 0, height: "45%", left: 0, position: "absolute", right: 0 },
  screen: { backgroundColor: "black", flex: 1 },
});
  • ThemeProvider with theme merges style over the theme it's given, here the app's theme from useTheme(). Only the list below it sees the transparent background and the dark bubbles. The composer keeps the app's theme.
  • The values are used as written, so the bubbles keep these colors in both color schemes.
  • The message menu still opens over the whole screen, from the app's OverlayProvider.

Theme object explains ThemeProvider, and Incoming and outgoing styles the bubble tokens. The Channel backgrounds UI cookbook puts an image behind a message list the same way.

Read more