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

Channel lists

A channel list is the column of conversations a user picks from. This page covers what users can do in it, which state backs it, and what each row action calls. How the list renders and which parts you can replace is on Channel list UI. How lists load and stay current is on Channel list paginators.

What users get

The user opens the app and sees their channels, the latest activity first. Each row shows the channel's avatar and name, the time and a preview of the last message, and a badge with the number of unread messages. A pin icon marks a channel they pinned, and a mute icon a channel they muted. Scrolling down loads more channels, and clicking a row opens the channel.

When the user points at a row, or moves focus into it, two buttons appear: one mutes the channel, and "more" opens a menu with Archive, Pin and Leave Channel. In a direct message channel the menu also has Block User. Each action confirms its result with a notification, such as "Channel archived".

The list keeps up as things change. When a message arrives, its channel moves to the top of a list sorted by the last message. When the user archives a channel, a list that leaves archived channels out drops it, and a list of archived channels picks it up.

How it works across the layers

State: the lists and their channels

A list is a ChannelPaginator: a query with filters and a sort order, and the channels it has loaded. The app registers it on client.channelManager. The manager keeps every registered list current as events arrive, and decides which list holds a channel that matches several. Channel list paginators explains both.

Each row is a Channel object, the same one the open channel and the rest of the app use. The row shows what the channel's stores hold, so a change made anywhere shows in the row too.

Three things about a channel belong to the current user alone: whether they archived it, pinned it or muted it.

  • Archived and pinned are dates on the user's membership, archived_at and pinned_at. A list can filter on archived and pinned and sort on pinned_at, and the client applies the same filters and sort when they change.
  • Muted is the channel's muteStatus. A muted channel's new messages don't count as unread.

What channels and threads hold has the fields and the events that change them.

UI: the list and its rows

ChannelNavigation renders the list header, the search bar, and one ChannelList for each paginator on the channel manager. While a search is active, the search results take the lists' place; see Search. To place lists yourself, render <ChannelList paginator={paginator} /> for each one.

ChannelList loads the first page when it mounts, unless its paginator has already loaded one, and the next page when the user scrolls near the end. loadMoreThresholdPx and loadMoreDebounceMs tune when the next page loads. It also starts the channel manager's event subscriptions, which every list shares. When a query fails, the list publishes an error notification.

Each row is a ChannelListItem. It reads its own channel's stores, so a row re-renders only when its channel changes. The unread badge is hidden while the channel is muted. Channel list UI lists what each part of a row reads.

The list holds no selection. Clicking a row calls openChannel(channel) from useWorkspaceNavigation(), and the layout decides where the channel opens. Without a layout, clicking a row does nothing; see Navigation.

The row actions

The row actions are ChannelListItemActionButtons. Each one calls a method of the row's channel:

Action Where What it calls Shown when
Mute, Unmute Next to row Channel.mute(), Channel.unmute() The user has the mute-channel capability
Archive, Unarchive Menu Channel.archive(), Channel.unarchive() The user is a member
Block User, Unblock User Menu Channel.banUser(otherUserId, {}), Channel.unbanUser(otherUserId) The channel has two members, the user is one of them, and has ban-channel-members
Pin, Unpin Menu Channel.pin(), Channel.unpin() The user is a member
Leave Channel Menu Channel.removeMembers({ members: [client.userId] }) The user has the leave-channel capability

Block User in this menu bans the other member from the channel. It isn't the same as client.blockUser(), which the message menu and channel details call. Flagging, muting, blocking and bans explains the difference.

None of these actions changes the row before the server answers. The row and the lists change when the server's event arrives:

  • Mute. notification.channel_mutes_updated updates the channel's muteStatus. The row shows the mute icon and hides the unread badge, and lists filtered on muted move the channel.
  • Archive and pin. member.updated updates the user's membership. Lists filtered on archived or pinned, or sorted on pinned_at, move the channel.
  • Leave. notification.removed_from_channel removes the channel from every list.

Each action reports its result through client.notifications: "Channel muted", "Channel archived", "Channel pinned", "Left channel" or "User blocked", or an error such as "Failed to leave channel". What's optimistic and what isn't lists which state changes wait for the server.

Hiding a channel

The rows have no Hide action, but the state layer has one. Channel.hide() hides the channel for the current user until a new message arrives in it, and Channel.show() shows it again. Channel.hide({ clear_history: true }) also clears the channel's history for the user.

A hidden channel leaves every list whose filters don't mention hidden, when the server's channel.hidden event arrives. It comes back on channel.visible.

Turning it on or off

Channel lists are always available. A list shows the channels that match its paginator's filters, so scope them to the user, for example with members: { $in: [userId] }. Without such a filter, the list can show channels the user isn't a member of.

Who sees which row action depends on the user's capabilities in the channel, which the server derives from their roles and the channel type's permissions. The table above lists them. Archive and pin need only membership, because they change the user's own settings. Change capabilities on the server; see Permissions reference.

To show the rows without actions, set a component that renders nothing on the ChannelListItemActionButtons key:

import type { PropsWithChildren } from "react";
import { WithComponents } from "stream-chat-react";

const NoRowActions = () => null;

// Declared outside the component, so WithComponents gets the same object on every render.
const overrides = { ChannelListItemActionButtons: NoRowActions };

export const WithoutRowActions = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);

Customizing

Which channels a list shows, and in what order

The paginator's filters and sort decide this. This sidebar shows an inbox with pinned channels on top, and the archived channels in a second list below it:

src/Sidebar.tsx
import { useEffect } from "react";
import { ChannelPaginator } from "stream-chat";
import type { ChannelFilters, SortParamRequest } from "stream-chat";
import { ChannelNavigation, useChatContext } from "stream-chat-react";

const userId = "YOUR_USER_ID";

// Pinned channels first, the most recently pinned on top, then the latest message first.
const sort: SortParamRequest[] = [
  { direction: -1, field: "pinned_at" },
  { direction: -1, field: "last_message_at" },
];

const inboxFilters: ChannelFilters = {
  archived: false,
  members: { $in: [userId] },
};
const archiveFilters: ChannelFilters = {
  archived: true,
  members: { $in: [userId] },
};

export const Sidebar = () => {
  const { client } = useChatContext();

  useEffect(() => {
    const inbox = new ChannelPaginator({
      client,
      filters: inboxFilters,
      id: "inbox",
      sort,
    });
    const archive = new ChannelPaginator({
      client,
      filters: archiveFilters,
      id: "archive",
      sort,
    });
    client.channelManager.insertPaginator({ paginator: inbox });
    client.channelManager.insertPaginator({ paginator: archive });
    return () => {
      client.channelManager.removePaginator(inbox);
      client.channelManager.removePaginator(archive);
    };
  }, [client]);

  // Renders the header, the search bar, the inbox and then the archive.
  return <ChannelNavigation />;
};

When the user archives a channel from the inbox, member.updated arrives, and the channel moves to the archive list. When they pin one, it moves to the top of its list.

A list whose filters or sort change has to load again from the first page; see Changing filters or sort.

What a row looks like

Replace the row, its look or its last-message line through WithComponents. Channel list UI lists the override keys and has an example, and the Preview UI recipe builds a custom row.

The row actions

The row actions have no prop: ChannelListItemActionButtons always renders the default actions, and the default set isn't exported. To change them, set your own component on the ChannelListItemActionButtons key. Channel-list row actions shows one, with its own capability check.

Where a channel opens

The layout decides. Moving between channels, threads and messages covers every way a channel opens, and the ⌘/ctrl-click opens beside recipe opens a channel next to the current one.

Reference