Livestream chat
Livestream chat sits next to a video stream, with thousands of viewers, a fast flow of short messages and a list that has to stay smooth for hours on a phone. This page covers which channel type to use and how to keep memory and rendering bounded. It also covers unread counts without read events, and which features to turn off. The Livestream message list UI cookbook builds the screen, with the video above or behind the chat.
What users get
Example
The user opens a concert stream. The video plays above the chat. The chat shows the newest messages and follows them as they arrive, several a second.
The user scrolls up to read a question. New messages keep arriving below, and the scroll-to-bottom button takes them back to the latest one. When they return to the bottom, the list follows the newest messages again.
An hour later, the app still holds only the newest few hundred messages, not every message of the stream.
How it works across the layers
Server: the livestream channel type
Stream has a built-in livestream channel type. Users don't have to be members to read and write. Any user with a valid token can watch the channel and post. Its url_enrichment setting is off by default.
Check the type's Read events setting (read_events) and turn it off. Livestream best practices recommends turning off read events, typing events and connect events on the type. It also explains the server's own message throttling. Channel types lists every setting.
The channel type's settings decide what the client may do. A setting that is off on the server is off in the client too, whatever client.config says. See Configuration.
State: the message paginator
The channel's messages live in channel.messagePaginator. Two of its settings matter for a busy channel:
stateThrottleMslimits how often the paginator publishes itsitems, and with them how often the list renders. It is 500 ms by default, so a burst of messages causes about two renders a second. Messages the user sends show at once.maxLoadedItemscaps how many messages the paginator keeps. It is off by default. See Capping the messages.
Throttling and the memory cap explains both.
State: read state without read events
With read_events off, the server keeps no read state for the channel, so nothing counts as unread. Turning read events off in Read state and unread counts explains what else changes. Counting unread messages locally shows how to bring back an unread count that lives only on the device.
UI: the message list
MessageList and MessageFlashList both take isLiveStreaming, which tunes the list for many messages a second. MessageFlashList renders with FlashList from @shopify/flash-list, which reuses the views of rows that scroll out for the rows that scroll in, where MessageList creates a view for every message. MessageFlashList is experimental, and so is isLiveStreaming on both lists. MessageList and MessageFlashList in Message list UI compares them, and Performance lists what isLiveStreaming changes and the Channel props that take parts out of every row.
The lists pause the memory cap while the user reads near the oldest loaded message, which Scrolling back explains.
Turning it on or off
Opening the channel
Channel watches the channel when it mounts, unless the channel is already watched and loaded with its latest messages, so a livestream screen needs no request of its own. It watches without presence: true, which a livestream doesn't need. This screen opens one livestream channel. It sits inside OverlayProvider and Chat:
import { useMemo } from "react";
import {
Channel,
MessageComposer,
MessageFlashList,
useChatContext,
} from "stream-chat-react-native";
export const LivestreamScreen = () => {
const { client } = useChatContext();
const channel = useMemo(
() => client.channel("livestream", "concert"),
[client],
);
return (
<Channel channel={channel}>
<MessageFlashList isLiveStreaming />
<MessageComposer />
</Channel>
);
};Channel sets the page size of the message list to 25 messages, unless you registered one in client.config. Later pages load as the user scrolls up.
Capping the messages
Set maxLoadedItems on the message paginators. Once the newest messages loaded exceed the cap, each new message drops the oldest one. To cap every channel, set it in client.config. Name pageSize with it, because a change to these keys drops the page size Channel sets:
import type { StreamChat } from "stream-chat";
export const capEveryChannel = (client: StreamChat) => {
client.config.set({
channel: { messagePaginator: { maxLoadedItems: 500, pageSize: 25 } },
});
};To cap only livestream channels, set it from a setup function. It runs for every channel, so it can check the channel's type:
import type { StreamChat } from "stream-chat";
export const capLivestreamChannels = (client: StreamChat) => {
client.config.setSetupFunction("channel", ({ channel }) => {
if (channel.type !== "livestream") return;
channel.messagePaginator.updateConfig({ maxLoadedItems: 500 });
});
};There is one channel setup function, so this one replaces any you set before; merge them into one. Setup functions in Configuration explains how they run.
Throttling and the memory cap in Message paginators explains how the cap behaves: which messages it never drops, and when it applies.
Scrolling back
Example
The cap is 500, and a page is 25 messages. The user scrolls back through the 500 loaded messages, and the next page loads, so the paginator holds 525. The next new message would drop the 26 oldest, which are the ones the user is reading. The list jumps.
MessageList and MessageFlashList prevent this. They pause the cap while the user is within a fifth of the cap from the oldest loaded message, here within 100 messages of it. While it's paused, the paginator keeps every message. Once the user is back nearer the newest messages, the next new message drops everything over the cap at once. A list of your own pauses the cap with MessagePaginator.setPruningSuspended(), and resumes it when the user returns.
Jumping to a message
A jump, for example to a quoted message or a pinned announcement, shows the messages around the target instead of the newest ones. The cap applies to neither the messages around the target nor the newest messages while they're off screen, so the newest messages keep growing as messages arrive. When the user jumps back to the latest message, the next new message brings the newest messages back down to the cap. A channel left after a jump opens on its newest messages again. Throttling and the memory cap in Message paginators explains when the cap applies.
Counting unread messages locally
Turn on channel.readEvents.localUnreadCountEnabled in client.config to count unread messages on this device, without the server. Turning read events off in Read state and unread counts explains what the local count counts, how it resets and who sees it:
import type { StreamChat } from "stream-chat";
export const countUnreadLocally = (client: StreamChat) => {
client.config.set({
channel: { readEvents: { localUnreadCountEnabled: true } },
});
};What the user sees
Without the local count, the UI has no unread count for the channel. Its row in a channel list has no unread badge, the scroll-to-bottom button has no number, and the list shows no unread notification.
With the local count:
- The channel's row in a channel list shows the unread count.
- The scroll-to-bottom button shows the number of messages that arrived while the user was scrolled up.
- The list shows the unread notification, and marks the channel read locally, which resets the count and sends nothing, when the user catches up.
In both cases the user can't mark a message unread. That action needs the read-events capability, which channels without read events don't give.
Turning off typing, presence and delivery receipts
Typing events are per channel type. Turn off typing_events on the livestream type on the server, and the client sends and shows none.
In the client, a messageComposer setup function can stop the composers of livestream channels from sending typing events:
import type { StreamChat } from "stream-chat";
export const noTypingEventsInLivestreams = (client: StreamChat) => {
client.config.setSetupFunction("messageComposer", ({ composer }) => {
if (composer.channel.type !== "livestream") return;
composer.updateConfig({ text: { publishTypingEvents: false } });
});
};To turn typing off in this client for every channel instead, set channel.typingEvents.enabled: false in client.config. Typing indicators explains the server and client settings.
For presence, Channel watches the channel without presence: true, and you turn off connect_events on the channel type.
Delivery receipts are per channel type too. When the type's delivery_events setting is on, every viewer reports delivery of the newest message through client.messageDeliveryReporter, about once a second while messages arrive. Turn off delivery_events on the livestream type, or set channel.deliveryEvents.enabled: false in client.config to stop this client from reporting delivery. Message delivery and read status explains the type setting.
Offline support
With enableOfflineSupport on Chat, every incoming message is written to the local database. A livestream list is ephemeral and moves forward only, so the writes cost more than they give, and Stream's livestream best practices recommend turning offline storage off. Offline support is off by default and applies to the whole app. Offline support explains what it stores.
Customizing
A lighter list
These Channel props take parts out of every row, at the cost of what those parts show. Performance explains each:
import type { PropsWithChildren } from "react";
import type { Channel as StreamChannel } from "stream-chat";
import { Channel } from "stream-chat-react-native";
export const LightLivestreamChannel = ({
channel,
children,
}: PropsWithChildren<{ channel: StreamChannel }>) => (
<Channel
channel={channel}
enableMessageGroupingByUser={false}
hideDateSeparators
hideStickyDateHeader
>
{children}
</Channel>
);Keep a custom message UI light too. The list mounts and measures each row as the user scrolls, so a message component with few nested views mounts faster.
Showing the viewer count
channel.state holds watcherCount, the number of users watching the channel. The server sends it with the channel query, and events from the channel can carry an update. useChannelOnlineMemberCount(channel) returns it and updates with it. This badge shows the count:
import { Text } from "react-native";
import {
useChannel,
useChannelOnlineMemberCount,
} from "stream-chat-react-native";
export const ViewerCount = () => {
const channel = useChannel();
const watcherCount = useChannelOnlineMemberCount(channel);
return <Text>{watcherCount} watching</Text>;
};Render it anywhere inside Channel, for example in a header of your own.
Announcements as pinned messages
Pin a message to keep it reachable, for example a link or the rules of the chat. The channel's pinned messages are in channel.pinnedMessagesPaginator, and jumping to one works as described in Jumping to a message.
The Pin and Unpin actions show only to users with the pin-message capability, so viewers who may not pin don't see them. Pinned messages explains who has it.
Slow mode
Slow mode makes each user wait between messages, which keeps a busy chat readable. Turn it on with Channel.enableSlowMode(seconds). Users with the skip-slow-mode capability, usually moderators and admins, don't wait. See Slow mode.
Moderation
From the message menu, viewers can flag a message and mute or block its author, and moderators can delete any message. The Flag action shows only for users with the flag-message capability, Delete with delete-any-message or delete-own-message, and Ban with ban-channel-members. Mute and Block have no capability check, so every viewer sees them on other users' messages. Flagging, muting, blocking and bans covers them. Block lists and automated moderation are set on the server, see Moderation.
Threads
A stream audience rarely needs threads. Turn replies off for the channel type on the server, or leave onThreadSelect off the message list and remove Thread Reply from the message menu, as the Livestream message list UI cookbook does.
Reference
- MessageList and MessageFlashList: the
isLiveStreamingprop. - Message paginator:
maxLoadedItems,stateThrottleMsandsetPruningSuspended. - Channel and channel state:
watcherCount,markReadLocallyandenableSlowMode. - Configuration tree:
channel.messagePaginator,channel.readEventsandclient.messageDelivery. - Performance: page size, throttling, the memory cap and measuring.
- Livestream best practices: the server side of a livestream.