Thread and thread manager
A Thread is the client's object for the replies to one message. client.threads, a ThreadManager, keeps the client's threads, the user's thread list and the unread thread count. This page lists the API of both in stream-chat.
What a thread holds is explained on What channels and threads hold. The thread list as a feature is on Replies and threads.
client.threads
The client creates client.threads when it's constructed. It keeps one Thread per parent message id, whether a thread is in the thread list or was opened from a message.
Getting a thread
| Call | Returns |
|---|---|
client.threads.ensure({ channel, parentMessage }) |
The thread of parentMessage, or a new one that client.threads keeps from then on. Sends no request. |
client.threads.get(id) |
The thread with that id, from the list or opened this session, or undefined. A thread's id is its parent message's id. |
client.getThreadAndHydrate({ message_id }, options) |
Loads one thread from the server. Resolves with the kept instance when there is one, and otherwise with a new object that client.threads doesn't keep. |
client.queryThreadsAndHydrate(options) |
Loads a page of the user's threads, with the same rule for each thread. The thread list uses it. |
Don't construct new Thread(), and don't keep the objects the last two return when client.threads.get() doesn't know them: events don't update them.
const thread = client.threads.ensure({ channel, parentMessage: message });
// The user opened the thread. Call release() when they close it.
const release = thread.activate();State
client.threads.state holds the user's threads as a whole:
| Field | Type | What it holds |
|---|---|---|
unreadThreadCount |
number |
How many of the user's threads have unread replies. The server sends it, so it counts threads not loaded. |
unseenThreadIds |
string[] |
Threads that got a reply since the thread list last loaded, and aren't in it. |
isThreadOrderStale |
boolean |
A listed thread got a reply since the list last loaded, so its order may be out of date. |
active |
boolean |
The thread list is on screen, set by client.threads.activate() and client.threads.deactivate(). |
wasActivatedAtLeastOnce |
boolean |
client.threads.activate() was called this session. Only then does a reconnect reload the list. |
The thread list
client.threads.paginator is a ThreadPaginator, a list like the others: its state store has items, isLoading, hasMoreTail and lastQueryError. Channel manager and ChannelPaginator lists what every list has. The thread list differs from the others:
- It loads 25 threads a page, with 10 participants and 10 replies each, and watches their channels.
- It keeps the server's order. There is no client-side sort, so a reply doesn't move a thread:
isThreadOrderStalebecomestrueinstead. hasMoreTailisfalseuntil the first page has loaded, soThreadPaginator.toTail()does nothing before that.ThreadPaginator.reload({ limit })loads the first page again and replaces the list once the response arrives. The threads stay on screen meanwhile. Only the first load setsisLoading.
Load it through client.threads.reload(), below, rather than through the paginator.
Methods
| Method | Returns | What it does |
|---|---|---|
ensure({ channel, parentMessage }) |
Thread |
See Getting a thread. |
get(id) |
Thread or undefined |
See Getting a thread. |
activate() |
void |
Sets active. While subscriptions are registered, it loads the list if needed. |
deactivate() |
void |
Clears active. |
reload({ force }) |
Promise<void> |
Loads the list when it hasn't loaded, has unseen threads or a stale order. A successful load clears unseenThreadIds and isThreadOrderStale. force: true loads it in any case. A call made during a reload does nothing. |
registerSubscriptions() |
void |
Starts handling events for the list and every kept thread. Calling it again does nothing. |
unregisterSubscriptions() |
symbol |
Stops handling events. |
resetState() |
void |
Releases every thread, empties the list and resets the state. |
config, configState |
ThreadManagerConfig |
The resolved configuration, and the same value as a store. |
updateConfig(patch) |
void |
Changes the configuration. |
client.threads.activate() and client.threads.deactivate() are a plain on and off flag, not counted. client.threads.registerSubscriptions() isn't counted either: one client.threads.unregisterSubscriptions() stops it. Code that uses stream-chat alone calls client.threads.registerSubscriptions() itself; the React SDK calls it for you.
The configuration has one setting, connectionRecoveryThrottleMs: the minimum time between reloads after reconnects, 1000 ms by default. It's read when the subscriptions are registered, so a change applies from the next client.threads.registerSubscriptions(). Register it under the client key:
client.config.set({
client: { threads: { connectionRecoveryThrottleMs: 2000 } },
});Events
While its subscriptions are registered, client.threads handles these events:
| Event | What changes |
|---|---|
health.check, connection.ok, notification.mark_read, notification.mark_unread, notification.thread_message_new, notification.channel_deleted |
unreadThreadCount, when the event carries it. |
notification.thread_message_new |
isThreadOrderStale for a listed thread, unseenThreadIds for another one, once the list has loaded. |
connection.recovered |
Reloads the list with force: true, if it was activated this session. |
message.new, message.read, notification.mark_unread, thread.updated, message.updated, message.undeleted, message.deleted, reaction.*, user.messages.deleted, user.deleted |
Hands each event to the threads it concerns. See Thread events. |
Thread state
A thread's identity and its objects:
| Property | What it is |
|---|---|
thread.id |
The parent message's id. |
thread.channel |
The channel the thread belongs to. |
thread.state |
The store described below. |
thread.messagePaginator |
The replies. See Message paginator. |
thread.messageComposer |
The composer for replies. See MessageComposer API. |
thread.messageOperations |
Sends, retries, edits and deletes replies. See Message operations. |
thread.state holds these fields. The timestamps are in nanoseconds:
| Field | Type | What it holds |
|---|---|---|
parentMessage |
LocalMessage |
The message the thread replies to. The same message the channel's list shows, so an edit shows in both. |
replyCount |
number |
How many replies the thread has, from the parent message's reply_count. |
participants |
ThreadParticipant[] or undefined |
The users who replied. |
read |
ThreadReadState |
Each participant's read state in the thread, by user id. The fields are in the next table. |
channel |
Channel |
The channel the thread belongs to. |
title |
string |
The thread's title. |
custom |
CustomThreadData |
The thread's custom data. |
createdAt |
TimestampNS |
When the thread was created. |
updatedAt |
TimestampNS or null |
When the thread was last updated. |
deletedAt |
TimestampNS or null |
When the thread or its parent message was deleted. |
active |
boolean |
Something has activated the thread with Thread.activate() and not released it yet. |
isLoading |
boolean |
Thread.reload() is running. |
isStateStale |
boolean |
The thread needs loading again: it knows only its parent message, or it missed events. |
A thread's read state uses camelCase, unlike a channel's:
| Field | What it holds |
|---|---|
user |
The user. |
lastReadAt |
When the user last read the thread. |
unreadMessageCount |
How many replies the user hasn't read. |
lastReadMessageId |
The last reply the user read, when known. |
firstUnreadMessageId |
The reply the user marked unread from, after a mark unread. |
Two getters read the state once, without subscribing:
thread.hasStaleStatereadsisStateStale.thread.ownUnreadCountreads the current user'sunreadMessageCount, or0.
This handler runs whenever the reply count or the current user's unread count changes:
const ownUserId = client.userId;
const unsubscribe = thread.state.subscribeWithSelector(
({ read, replyCount }) => ({
replyCount,
unread: ownUserId ? (read[ownUserId]?.unreadMessageCount ?? 0) : 0,
}),
({ replyCount, unread }) => {
console.log(`${replyCount} replies, ${unread} unread`);
},
);Thread events
A thread doesn't listen to the client itself. client.threads hands it the events about it, as long as the thread is kept and the manager's subscriptions are registered:
| Event | What changes |
|---|---|
message.new with the thread's parent_id |
The reply joins the list. Each unreadMessageCount in read grows, except the sender's and, while the thread is active, the current user's. |
message.read for the thread |
The reader's entry in read. |
notification.mark_unread for the thread |
The reader's entry in read. |
thread.updated |
title, custom, updatedAt and deletedAt. |
message.updated, message.undeleted, message.deleted |
The reply in the list, or the parent message. |
reaction.new, reaction.updated, reaction.deleted |
Replies that quote the message. |
user.messages.deleted, user.deleted |
That user's replies, in every kept thread. |
A change to the parent message from anywhere, such as a reaction, also updates parentMessage, replyCount, participants and deletedAt.
Thread methods
| Method | Returns | What it does |
|---|---|---|
activate() |
() => void |
Marks the thread active and returns the function that releases it. Activations are counted. The first one also keeps the thread in client.threads for the session. |
markRead({ force }) |
A promise of the response, or null |
Marks the thread read through client.messageDeliveryReporter, with requestHandlers.markReadRequest when registered. Resolves with null without a request while the user has no unread replies, unless force is true. |
reload() |
Promise<void> |
Loads the thread again and refreshes its loaded replies, keeping replies that failed to send. Does nothing while a reload runs. |
registerSubscriptions() |
void |
Starts the thread's own subscriptions. client.threads calls it for every thread it keeps. |
unregisterSubscriptions() |
symbol |
Stops them and marks the thread stale. |
config, configState |
ThreadConfig |
The resolved configuration, and the same value as a store. |
updateConfig(patch) |
void |
Changes the configuration. |
While a thread is active and its subscriptions are registered:
- a reply that would be unread is marked read instead, through
Thread.markRead(), throttled; - when its state is stale, it reloads.
Thread.reload() rejects when the request fails, except for a thread that has no replies yet: the server doesn't know it, so that answer is ignored. A connection that stays up while the channel stops being watched marks the thread stale. After a reconnect, the client reloads active threads and marks the others stale.
The configuration has one setting, requestHandlers.markReadRequest. It replaces the request Thread.markRead() sends, and is registered under the thread key:
client.config.set({
thread: {
requestHandlers: {
markReadRequest: async ({ options, thread }) => {
console.log("Marking read", thread.id);
return await thread.channel.markRead(options);
},
},
},
});The thread key also holds messagePaginator and messageOperations, which go to the thread's reply list and message operations. See Configuration tree.
Threads also have public methods that client.threads calls to apply events: handleNewReply, handleRepliesRead, handleRepliesUnread, handleThreadUpdated, handleMessageUpdated, handleMessageDeleted, handleReactionChanged, handleUserMessagesDeleted, upsertReplyLocally, deleteReplyLocally, updateParentMessageLocally, updateParentMessageOrReplyLocally and hydrateState. App code doesn't call them. Thread.markAsRead() is a deprecated name for Thread.markRead().
See also
- What channels and threads hold explains a thread's state.
- Read state and unread counts explains thread unread counts.
- Message paginator lists the API of a thread's replies.
- Replies and threads shows the thread list and threads in the UI.