What channels and threads hold
A channel and a thread hold their conversations the same way. Each has its own state store, message paginator, composer, message operations and read state. A thread still belongs to a channel: it's identified by a message in that channel, and a few things, such as members, capabilities and settings, exist only on the channel.
Example
Take the channel "Design", and the thread under the user's message "Release plan" in it. A reply the user types in the thread goes through the thread's composer into the thread's message paginator. The channel's message paginator and composer are untouched.
This page lists what they share, then what belongs only to a channel or only to a thread. Getting a channel or a thread, and loading it, is on Shared entities.
What channels and threads share
| Channel | Thread | |
|---|---|---|
| State | channel.state |
thread.state |
| Message paginator | channel.messagePaginator |
thread.messagePaginator |
| Composer | channel.messageComposer |
thread.messageComposer |
| Message operations | channel.messageOperations |
thread.messageOperations |
| Read state | read in channel.state |
read in thread.state |
| Typing | typing in channel.state |
typing in channel.state, the entries with the thread's parent_id |
A thread belongs to its channel, thread.channel. thread.state has no typing field: who's typing in a thread is in the typing field of channel.state, which maps each typing user's id to their typing event. Keep the entries whose parent_id is the thread's id.
Each state is one store. Read a field once with getLatestValue(), or subscribe and select the fields you need:
const ownUserId = client.userId;
const unsubscribe = channel.state.subscribeWithSelector(
({ memberCount, read }) => ({
memberCount,
unread: ownUserId ? (read[ownUserId]?.unread_messages ?? 0) : 0,
}),
({ memberCount, unread }) => {
console.log(`${memberCount} members, ${unread} unread`);
},
);Where each part is explained
- The message paginator: Message paginators.
- The composer: Message composer.
- Message operations: Message operations.
- Read state: Read state and unread counts.
- Typing: Typing indicators.
- What keeps a channel or a thread in memory: Entity lifecycle.
Channel
Data and members
channel.state holds these fields:
| Field | What it holds |
|---|---|
data |
The channel's server data: frozen, hidden, cooldown, member_count, team, custom and the other fields. |
members |
The loaded members, by user id. A large channel loads only some of them: to search all of them, use a ChannelMemberSearchSource, see Search sources. |
memberCount |
The channel's total member count. |
membership |
The current user's own membership. |
watchers, watcherCount |
Who is watching the channel right now. |
watchStatus |
Whether this client watches the channel: watching, wasWatching (a reconnect ended the watch, see Connection) or notWatching. |
read |
Every member's read state. |
typing |
Who is typing. |
ownCapabilities |
What the current user may do in the channel. |
muteStatus |
Whether the current user muted the channel. |
initialized, offlineMode |
Whether the channel has been loaded from the server, or only from local data. |
active |
Whether something has opened the channel, with Channel.activate(). |
aiState |
The state of an AI response in progress, see SDK integration. |
channel.data, channel.state.members and the other getters on channel and channel.state return the value at the moment you read them. They don't tell you when it changes. To follow a change, subscribe to channel.state and select the field. Reactive state explains why.
Membership
membership is the current user's membership in the channel: their role, when they joined, and two dates the user sets for themselves:
archived_atis set while the user has archived the channel;pinned_atis set while the user has pinned it.
Channel.archive(), Channel.unarchive(), Channel.pin() and Channel.unpin() change them. The change shows when the server's member.updated event arrives, not when the call returns. Channel paginators filtered on archived or pinned move the channel at the same moment.
const unsubscribe = channel.state.subscribeWithSelector(
({ membership }) => ({
archived: !!membership.archived_at,
pinned: !!membership.pinned_at,
}),
({ archived, pinned }) => {
console.log({ archived, pinned });
},
);
await channel.archive(); // the handler runs again when member.updated arrivesCapabilities
Capabilities say what the current user may do in this channel, such as send-message, upload-file, send-reaction or skip-slow-mode. The server derives them from the user's roles and the channel type's permissions.
They're in the ownCapabilities field of channel.state, and they update when the channel is loaded again or when channel.updated carries new ones. When a query or Channel.updatePartial() returns different capabilities, the client also dispatches capabilities.changed.
const canReact = channel.state
.getLatestValue()
.ownCapabilities.includes("send-reaction");Settings
Settings say which features the channel has, such as typing events, read events and replies. There are two views of them:
channel.configis what's in effect: the channel type's settings combined with what the app registered inclient.config.channel.configStateis the same value as a store.channel.serverConfigis the channel type's settings as the server sent them:typing_events,read_events,replies,uploadsand the others. It'sundefineduntil the channel has been loaded.
Check channel.config, not channel.serverConfig. Either side can turn a feature off, and only config includes both:
const { readEvents, replies, typingEvents } = channel.config;
console.log({
readEvents: readEvents.enabled,
replies: replies.enabled,
typingEvents: typingEvents.enabled,
});Configuration explains which settings the client decides, which the server decides, and which need both.
Mute
The muteStatus field of channel.state says whether the current user muted the channel: { muted, createdAt, expiresAt }, with the dates in nanoseconds or null.
Channel.mute() and Channel.unmute() change it. Channel.mute({ expiration }) mutes for that many milliseconds. Like membership, the field changes when the server's event arrives, notification.channel_mutes_updated. A muted channel's new messages don't count as unread.
Slow mode
In slow mode, a user waits a number of seconds between messages. The channel's data.cooldown holds the number of seconds, and Channel.enableSlowMode(seconds) and Channel.disableSlowMode() change it.
Each channel has a cooldown timer, channel.cooldownTimer. Its state counts down for the current user:
| Field | What it holds |
|---|---|
cooldownConfigSeconds |
The channel's cooldown, in seconds. 0 without slow mode. |
cooldownRemaining |
Whole seconds until the user can send again. |
canSkipCooldown |
Whether the user has the skip-slow-mode capability. |
ownLatestMessageTimestamp |
When the user last sent a message, in nanoseconds. |
const unsubscribe = channel.cooldownTimer.state.subscribeWithSelector(
({ cooldownRemaining }) => ({ cooldownRemaining }),
({ cooldownRemaining }) => {
console.log(
cooldownRemaining > 0 ? `Wait ${cooldownRemaining}s` : "You can send",
);
},
);Slow mode shows how the composer uses it.
Thread
Parent message and replies
A thread is identified by its parent message: thread.id is the parent message's id. thread.state holds:
| Field | What it holds |
|---|---|
parentMessage |
The message the thread replies to. |
replyCount |
How many replies the thread has. |
participants |
The users taking part in the thread. |
read |
Every participant's read state in the thread. |
channel |
The channel the thread belongs to. |
title, custom |
The thread's own data. |
active |
Whether something has opened the thread, with Thread.activate(). |
isLoading, isStateStale |
Whether the thread is loading, and whether it needs loading again. |
The parent message is the same message object the channel's message paginator holds. An edit or a reaction on it shows in both places, and parentMessage and replyCount follow it.
The replies are in thread.messagePaginator. A reply also sent to the channel, with show_in_channel, is in the channel's message paginator too.
client.threads.ensure({ channel, parentMessage }) returns the thread for a message, creating it if the client doesn't have it yet, without a request. A thread created this way knows only its parent message. When it may have replies the client doesn't hold, it loads them the first time it's activated, which the UI does when it opens the thread:
const thread = client.threads.ensure({ channel, parentMessage: message });
// The user opened the thread. Call release() when they close it.
const release = thread.activate();While a thread is active:
- a new reply doesn't count as unread for the current user, and the thread marks itself read;
- when its state is stale, it loads again.
These happen only while the thread manager, client.threads, listens to events: after ThreadManager.registerSubscriptions(). The React SDK calls it for you.
The user's threads
The thread manager, client.threads, keeps the user's threads. Its paginator, client.threads.paginator, loads the threads, and client.threads.state holds what applies to all of them:
| Field | What it holds |
|---|---|
unreadThreadCount |
How many of the user's threads have unread replies. The server sends this number, so it counts threads the client hasn't loaded. |
unseenThreadIds |
Threads that got a new reply since the thread paginator last loaded, and aren't in it. |
isThreadOrderStale |
Whether a thread in the paginator got a reply, so their order may be out of date. |
ThreadManager.reload() loads the thread paginator again when it hasn't loaded yet, when there are unseen threads, or when the order is stale, and clears both once the load succeeds. ThreadManager.reload({ force: true }) loads it in any case.
const unsubscribe = client.threads.state.subscribeWithSelector(
({ unreadThreadCount, unseenThreadIds }) => ({
unreadThreadCount,
unseenThreadIds,
}),
({ unreadThreadCount, unseenThreadIds }) => {
console.log(
`${unreadThreadCount} unread threads, ${unseenThreadIds.length} new`,
);
},
);The thread list itself is on Replies and threads, and its API on Thread and thread manager.