Thread header
This recipe builds a thread header of its own: the avatars of the people in the thread, the title, and a subtitle with the channel's name and the reply count, or who is typing a reply. The default header is explained in Thread UI.
Best practices
- Read the thread from
useThreadContext()and subscribe tothread.statefor the reply count and the participants. Declare the selectors at module scope. - Take the channel's name from
useChannelPreviewInfo(), as the default header does. - Keep the back and close buttons,
WorkspaceNavigationBackButtonandWorkspaceNavigationCloseButton. They render only when the layout allows going back or closing the thread's panel. A button of your own closes the thread withuseCloseThread(). - Show typing from
channel.state.typing, filtered by the thread's id.TypingIndicatorHeadershows nothing inside a thread, because only the channel's composer receives typing events. - Keep the header compact, so the replies keep the space.
Create the component
import type { ChannelStateData, EventPayload, ThreadState } from "stream-chat";
import {
Avatar,
useChannel,
useChannelPreviewInfo,
useChatContext,
useStateStore,
useThreadContext,
WorkspaceNavigationBackButton,
WorkspaceNavigationCloseButton,
} from "stream-chat-react";
import "./CustomThreadHeader.css";
const threadStateSelector = ({ participants, replyCount }: ThreadState) => ({
participants,
replyCount,
});
const typingSelector = ({ typing }: ChannelStateData) => ({ typing });
export const CustomThreadHeader = () => {
const { client } = useChatContext();
const channel = useChannel();
const thread = useThreadContext();
const { participants = [], replyCount = 0 } =
useStateStore(thread?.state, threadStateSelector) ?? {};
const { typing } = useStateStore(channel.state, typingSelector);
const { displayTitle } = useChannelPreviewInfo({ channel });
// Others typing a reply in this thread. Typing events carry the thread's id as parent_id.
const typingNames = (Object.values(typing) as EventPayload<"typing.start">[])
.filter(
({ parent_id, user }) =>
parent_id === thread?.id && user?.id !== client.userID,
)
.map(({ user }) => user?.name || user?.id);
return (
<div className="custom-thread-header">
<WorkspaceNavigationBackButton />
<div className="custom-thread-header__participants">
{participants.map(({ user }) =>
user ? (
<Avatar
imageUrl={user.image}
key={user.id}
size="sm"
userName={user.name ?? user.id}
/>
) : null,
)}
</div>
<div className="custom-thread-header__content">
<div className="custom-thread-header__title">Thread</div>
<div className="custom-thread-header__subtitle">
{typingNames.length > 0
? `${typingNames.join(", ")} ${typingNames.length === 1 ? "is" : "are"} typing…`
: `${displayTitle} · ${replyCount} replies`}
</div>
</div>
<WorkspaceNavigationCloseButton />
</div>
);
};.custom-thread-header {
align-items: center;
display: flex;
gap: 12px;
padding: 12px 16px;
}
.custom-thread-header__participants {
display: flex;
gap: 4px;
}
.custom-thread-header__content {
flex: 1;
min-width: 0;
}
.custom-thread-header__title {
font-weight: 600;
}
.custom-thread-header__subtitle {
color: #6b7280;
font-size: 0.875rem;
}Here is what each part reads:
useThreadContext()returns the thread thatThreadorThreadSlotrenders. Itsstateholds the participants, each with auser, and the reply count. What channels and threads hold explains the rest.useChannel()returns the thread's channel, souseChannelPreviewInfo()gives its name.channel.state.typingholds the latest typing event of each user in the channel. An event sent from a thread carries the thread's id asparent_id. The filter is the one from Typing in threads, which also shows a typing indicator for the thread's message list.
The typing text here is plain English. To translate it, use t from useTranslationContext(), as Localization explains.
Render it in the thread
ThreadHeader has no override key. Thread renders only its children, so put the header where ThreadHeader would go. With ChatView, that's inside ThreadSlot:
import { MessageComposer, MessageList } from "stream-chat-react";
import { ThreadSlot } from "stream-chat-react/slot-layout";
import { CustomThreadHeader } from "./CustomThreadHeader";
export const ThreadPanel = () => (
<ThreadSlot slot="thread">
<CustomThreadHeader />
<MessageList />
<MessageComposer />
</ThreadSlot>
);Render ThreadPanel in the views of ChatView, as in Add threads. Without a layout, render the header inside <Thread thread={thread}> the same way.