Thread UI
A thread is a message and its replies, shown apart from the channel. Thread shows one thread, and the user's threads are listed on Thread list UI. This page covers Thread, the parent message, a thread header, replies sent to the channel too, and the parts you can replace. The props are in the Thread reference.
The Thread component
<Thread thread={thread}> shows the thread passed as thread. Like Channel, it renders the parts you give it as children:
import type { Thread as StreamThread } from "stream-chat";
import { MessageComposer, MessageList, Thread } from "stream-chat-react-native";
export const ThreadScreen = ({ thread }: { thread: StreamThread }) => (
<Thread thread={thread}>
<MessageList />
<MessageComposer />
</Thread>
);Here is what Thread does:
- It gives the thread to its children.
useThreadContext()returns it, anduseChannel()returns the thread's channel, so aThreadneeds noChannelaround it. Render it on a screen of its own, next to the channel's screen. - The children then work on the thread's objects.
MessageListrenders the replies inthread.messagePaginator, andMessageComposerwrites intothread.messageComposer. - Around the children it renders what
Channelrenders: the keyboard handling, the contexts the message list and composer read, and the attachment picker. It takes the same configuration props asChannel, which it doesn't inherit from one; see Configuration shared with Thread. - It loads the first page of replies, unless the thread already holds some. A thread from the thread list arrives with its latest replies.
- It shows the newest replies when the replies were left on an older page, for example after a jump to a reply, unless a jump to a reply already landed or is still loading. When the newest page is still loaded, this needs no request. It's the same behavior as
Channelhas for its messages; see The Channel component. - It reloads the thread while its state is stale: when the thread was created from a message that has replies or in a channel that isn't watched, or after the thread stopped receiving its channel's events.
- It watches the thread's channel once when nobody has loaded it, which happens for a thread opened from the thread list. That gives the user's capabilities in the channel to the message menu, the reactions and the composer.
- It activates the thread while it's mounted. An active thread receives its events even when the thread list doesn't hold it, and marks new replies read as they arrive.
- When it unmounts, it calls
onThreadDismount, for example to clear your app's record of the open thread.
When the first page of replies fails to load, Thread renders LoadingErrorIndicator in place of its children, "Error loading replies...". Tapping it loads the replies again.
Get the thread from the client, never with new Thread(). onThreadSelect on MessageList and ThreadList gives you the thread the user picked, client.threads.ensure({ channel, parentMessage }) returns the thread for a message and creates it when needed, and client.threads.get(id) returns one the client already has. Either way the client keeps one instance per thread and sends it its events. What channels and threads hold explains the thread's state.
The SDK doesn't navigate: the replies button on a message calls onThreadSelect on the message list, and your app shows the thread's screen, as in Quick start. Without onThreadSelect, the button does nothing.
What's different in a thread
The message list and the composer behave as in a channel, with these differences:
- The unread notification and the unread count on the scroll-to-bottom button don't show. The thread marks itself read while it's open.
- A thread with no replies shows its parent message rather than the empty state.
- The attachment picker has no poll button: polls can't be created in a thread.
- The composer shows the also-send-to-channel checkbox.
- The typing indicator shows the users typing in this thread only.
- The message menu offers Quote Reply, and the quote goes into the thread's composer. Thread Reply and Mark as unread aren't offered.
- The message list follows new replies, and scrolls to the user's own reply when they send it.
The parent message
The parent message isn't a child you place. The message list renders it above the replies, because it has to scroll with them: the message, a "4 Replies" divider under it, and a spinner while older replies load. It's the list's older-end component, ThreadFooterComponent, which reads the parent message and the reply count from thread.state, so it updates when they change. Tapping or long-pressing the parent message does nothing. When the current user sent it, it shows the sending, sent, delivered or read mark like any of the user's messages, as MessageStatus explains on Message UI.
To change it, pass your own component as the list's prop for the older end, FooterComponent on MessageList or HeaderComponent on MessageFlashList. ThreadFooterComponent is exported, so your component can render it with your additions around it. It takes parentMessagePreventPress={false} to make the parent message pressable.
A thread header
The SDK has no thread header component. Set the native header's title through the screen's options, or render a header of your own as the first child of Thread, as for a channel (see The channel header). Inside Thread, useTypingString() returns who is typing in the thread, and thread.state holds the parent message:
import { StyleSheet, Text, View } from "react-native";
import type { ThreadState } from "stream-chat";
import {
useStateStore,
useThreadContext,
useTypingString,
} from "stream-chat-react-native";
const parentMessageSelector = ({ parentMessage }: ThreadState) => ({
parentMessage,
});
export const ThreadHeader = () => {
const { thread } = useThreadContext();
const { parentMessage } =
useStateStore(thread?.state, parentMessageSelector) ?? {};
const typing = useTypingString();
const author = parentMessage?.user?.name ?? parentMessage?.user?.id;
return (
<View style={styles.container}>
<Text style={styles.title}>Thread</Text>
<Text numberOfLines={1}>
{typing || (author ? `with ${author}` : "")}
</Text>
</View>
);
};
const styles = StyleSheet.create({
container: { alignItems: "center", paddingVertical: 8 },
title: { fontSize: 16, fontWeight: "600" },
});Replies sent to the channel
In a thread, the composer shows a checkbox, "Also send to channel". With it checked, the reply is also posted to the channel's message list. It toggles showReplyInChannel in the thread composer's state. allowThreadMessagesInChannel={false} on Thread hides it.
A reply sent to both places shows a mark above it, SentToChannelHeader: "Replied to a thread" in the channel, and "Also sent in channel" in the thread. The mark gets a "View" link when you pass onAlsoSentToChannelHeaderPress to Channel or Thread. In the channel, the handler gets the reply's id and its thread, so your app can open the thread at that reply. In the thread, it gets the reply's id, so your app can open the channel at that message. A jump to the reply is a paginator call: see Jumps on screen.
Replacing parts
Register replacements with WithComponents. Overriding components explains how overrides apply.
| Override key | Replaces |
|---|---|
SentToChannelHeader |
The mark on a reply sent to the channel too |
LoadingErrorIndicator |
The error state when the first page of replies fails to load |
Thread has no other override key: render other children in it. The parent message is the list's prop, as The parent message explains. The also-send-to-channel checkbox is part of InputView; replace that key to change it. Inside a thread, the message list and the composer take the same override keys as anywhere else; Message list UI and Message composer UI list them.