Info:
This is release candidate documentation for Stream Chat React SDK v15. For the latest stable version, see the latest version (v14).

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.

Info:

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

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_at is set while the user has archived the channel;
  • pinned_at is 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 arrives

Capabilities

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.config is what's in effect: the channel type's settings combined with what the app registered in client.config. channel.configState is the same value as a store.
  • channel.serverConfig is the channel type's settings as the server sent them: typing_events, read_events, replies, uploads and the others. It's undefined until 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.

Used in