# Livestream message list

> **Info:** This is release candidate documentation for **Stream Chat React Native SDK v10**. For the latest stable version, see the [latest version (v9)](https://getstream.io/chat/docs/sdk/react-native/).

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](https://getstream.io/chat/docs/sdk/react-native/v10/features/app-experience/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](https://getstream.io/chat/docs/sdk/react-native/v10/features/app-experience/performance/#channel-settings) lists what each setting costs, and [Slow mode](https://getstream.io/chat/docs/sdk/react-native/v10/features/channels/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:

```ts title="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](https://getstream.io/chat/docs/sdk/react-native/v10/state/channels-and-threads/read-state-and-unread-counts/#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](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/message-paginators/#throttling-and-the-memory-cap) explains both paginator settings, and [Configuration](https://getstream.io/chat/docs/sdk/react-native/v10/state/state-and-entities/configuration/#setup-functions) 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.

```tsx title="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" },
});
```

<table>
  <tr>
  <td align='center' width='50%'>

![A black video area above a chat list and composer on iOS](https://getstream.io/docs-assets/images/fbe96de93a2e.png)

  </td>
  <td align='center' width='50%'>

![A black video area above a chat list and composer on Android](https://getstream.io/docs-assets/images/c0f1b5a287ec.png)

  </td>
  </tr>
</table>

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](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/overlays-and-bottom-sheets/#fixed-ui-above-a-closing-menu) explains.

[MessageList and MessageFlashList](https://getstream.io/chat/docs/sdk/react-native/v10/ui/components/message-list-ui/#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:

```tsx title="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](https://getstream.io/chat/docs/sdk/react-native/v10/reference/theme/theme-object/) explains `ThemeProvider`, and [Incoming and outgoing styles](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/message/incoming-and-outgoing-styles/) the bubble tokens. The [Channel backgrounds](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/channel-list/channel-list-background/) UI cookbook puts an image behind a message list the same way.

## Read more

- [Performance](https://getstream.io/chat/docs/sdk/react-native/v10/features/app-experience/performance/): page size, throttling, the memory cap and measuring.
- [Read state and unread counts](https://getstream.io/chat/docs/sdk/react-native/v10/state/channels-and-threads/read-state-and-unread-counts/): what counts as unread, and the local count.
- [Message list UI](https://getstream.io/chat/docs/sdk/react-native/v10/ui/components/message-list-ui/): what the list renders and when it loads.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/message/livestream-message-list/](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/message/livestream-message-list/).