Thread UI
A thread is a message and its replies, shown apart from the channel. Two parts of the UI show threads: Thread, which shows one thread, and ThreadList, which lists the user's threads. This page covers both, the thread header, and the parts you can replace. The props are in the Thread and ThreadList references.
The Thread component
<Thread thread={thread}> shows the thread passed as thread. Like Channel, it renders no UI of its own, only its children:
import type { Thread as StreamThread } from "stream-chat";
import {
MessageComposer,
MessageList,
Thread,
ThreadHeader,
} from "stream-chat-react";
export const ThreadPanel = ({ thread }: { thread: StreamThread }) => (
<Thread thread={thread}>
<ThreadHeader />
<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. - The children then work on the thread's objects.
MessageListrenders the replies inthread.messagePaginator, andMessageComposerwrites intothread.messageComposer. - It loads the thread while its state is stale. A thread created from its parent message starts stale when it may have replies to load. A thread from the thread list arrives loaded.
- It renders nothing until the thread has its parent message, and nothing when replies are turned off for the channel (
replies.enabledin its configuration). - It renders the thread's container,
.str-chat__thread, around the children.
The parent message isn't a child you place. MessageList renders it above the replies, because it has to scroll with them: a date separator, the message, and the reply count under it.
Get the thread from client.threads, not with new Thread(). client.threads.ensure({ channel, parentMessage }) returns the thread for a message, creating it if 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.
With ChatView, ThreadSlot renders Thread for the thread in its slot, as in Add threads. Without a layout, the replies button on a message does nothing, so the app picks the thread and renders Thread itself.
The thread header
ThreadHeader shows, from start to end:
StartContent, by default the back button,WorkspaceNavigationBackButton. It shows only when the thread is stacked over other content in its panel.- The title, "Thread", with a subtitle: the channel's name and the number of replies, such as "Design · 4 replies". The
overrideTitleprop replaces the channel's name. EndContent, by default the close button,WorkspaceNavigationCloseButton. It shows only when the thread's panel can be closed, and closes it withuseCloseThread().
Like the channel header's buttons, both come from the layout, and neither renders without one. A component passed as StartContent or EndContent replaces the button; render the button yourself to keep it. The Thread header recipe builds a header of its own.
The thread list
ThreadList lists the threads the user takes part in, in the server's order: unread threads first, then by latest reply. It reads client.threads:
- When it mounts, it reloads the list. While the page is visible, it activates
client.threads, so the list follows new replies; it deactivates it when the page is hidden or the list unmounts. - It subscribes to
client.threads.paginator.stateand renders aThreadListItemfor each thread. It loads the next page when the user scrolls to the end. - Clicking a row opens the thread through
openThread(thread)fromuseWorkspaceNavigation(). ⌘/ctrl-click asks the layout to open it beside the current one, where the layout has room.
Each row reads its thread's stores: thread.state for the parent message, the channel, the participants, the reply count and the user's unread count, and thread.messagePaginator.aggregateState for the latest reply. ThreadListItem renders ThreadListItemUI, the row's look. Anything inside a row can read its thread with useThreadListItemContext().
The list header shows the "Threads" title. Like the channel list header, it renders the HeaderEndContent override at its end while a thread is open.
The unseen threads banner
A thread that isn't in the loaded list can get a reply, for example an older thread someone answers. The thread manager records it in client.threads.state as unseen, and the list shows a banner above the rows: "2 unread threads". Clicking it reloads the list, which brings those threads in at their new position.
Loading, empty and error states
| State | What the list renders | Override key |
|---|---|---|
| Loading the first page | Placeholder rows | None |
| Loading the next page | A spinner after the last row | ThreadListLoadingIndicator |
| No threads | "Reply to a message to start a thread" | ThreadListEmptyPlaceholder |
A reload keeps the loaded threads on screen until the new ones arrive. When a query fails, the list keeps what it had loaded, or shows the empty placeholder if it had nothing. The error is recorded as lastQueryError in client.threads.paginator.state; the list shows no error and publishes no notification.
Replacing parts
Register replacements with WithComponents. Overriding components explains how overrides apply.
| Override key | Replaces |
|---|---|
ThreadHead |
The parent message above the replies. Gets the parent message as message. |
ThreadStart |
The reply count under the parent message |
ThreadListItem |
The whole row. Gets thread. |
ThreadListItemUI |
The row's look |
ThreadListUnseenThreadsBanner |
The unseen threads banner |
ThreadListLoadingIndicator, ThreadListEmptyPlaceholder |
The states above |
HeaderEndContent |
The end of the list header, while a thread is open |
The defaults of ThreadHead, ThreadListUnseenThreadsBanner, ThreadListEmptyPlaceholder and ThreadListLoadingIndicator aren't exported, so a replacement for one of them can't render the default inside it.
Thread and ThreadHeader have no override key. Render other children in Thread, and change ThreadHeader through its props. 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.
This thread list keeps the default row and shows the thread's title above it, when the thread has one:
import type { ThreadState } from "stream-chat";
import type { ThreadListItemUIProps } from "stream-chat-react";
import {
ThreadList,
ThreadListItemUI,
useStateStore,
useThreadListItemContext,
WithComponents,
} from "stream-chat-react";
const titleSelector = ({ title }: ThreadState) => ({ title });
const RowWithTitle = (props: ThreadListItemUIProps) => {
const thread = useThreadListItemContext();
const { title } = useStateStore(thread?.state, titleSelector) ?? {};
return (
<div className="thread-row">
{title && <span className="thread-row__title">{title}</span>}
<ThreadListItemUI {...props} />
</div>
);
};
const overrides = { ThreadListItemUI: RowWithTitle };
export const ThreadsSidebar = () => (
<WithComponents overrides={overrides}>
<ThreadList />
</WithComponents>
);