Members and online status

This recipe renders the members of the open channel with their online status.

Note:

What you'll build

  • A member list for the open channel
  • A green dot beside each member who is online
  • A list that updates as members come online, go offline, join or leave

Preview

The channel header above a list of the channel members, with a green dot for each member who is online

Approach: Compose. Choosing where to start explains the approaches.

Starts from: the base app. The page shows only what the recipe adds.

Best practices

  • Read members from channel.state with useChannelMembersState(channel). It subscribes to the store, so the component re-renders when the members change.
  • Watch the channel with presence, so the server sends presence changes. Without it, member.user.online shows the value from when the channel loaded.
  • Don't add your own user.presence.changed listener. The client already writes each presence change into the members it holds.
  • Keep the list simple. With presence on, it re-renders every time a member comes online or goes offline.
  • Expect only some members in a large channel. A channel loads a limited number of members with its query; see Channel details and members for the full member list.

Render the channel members list

The component reads the channel from Channel with useChannel(), and its members with useChannelMembersState:

src/ChannelMembers.tsx
import {
  useChannel,
  useChannelMembersState,
  VisuallyHidden,
} from "stream-chat-react";
import "./ChannelMembers.css";

export const ChannelMembers = () => {
  const channel = useChannel();
  const members = useChannelMembersState(channel);

  return (
    <ul className="cookbook-channel-members">
      {Object.values(members).map((member) => (
        <li className="cookbook-channel-members__item" key={member.user_id}>
          <span
            aria-hidden
            className={
              member.user?.online
                ? "cookbook-channel-members__dot cookbook-channel-members__dot--online"
                : "cookbook-channel-members__dot"
            }
          />
          {member.user?.name ?? member.user_id}
          <VisuallyHidden>
            {member.user?.online ? ", online" : ", offline"}
          </VisuallyHidden>
        </li>
      ))}
    </ul>
  );
};

The dot is only decoration for screen readers, so it is aria-hidden; the SDK's VisuallyHidden adds the status as text that screen readers read after the name and the page doesn't show.

The rules sit in the stream-overrides layer, so they win over the SDK's own rules once the SDK's stylesheet is in an earlier layer, as Theming → CSS layers sets up. They take their colors, spacing and type from the SDK's tokens, so they follow your theme: the dots use the presence colors, --str-chat__presence-bg-online and --str-chat__presence-bg-offline, that the SDK's avatars use.

Render it inside Channel, next to the other parts of the channel:

src/ChannelPanel.tsx
import type { Channel as StreamChannel } from "stream-chat";
import {
  Channel,
  ChannelHeader,
  MessageComposer,
  MessageList,
} from "stream-chat-react";
import { ChannelMembers } from "./ChannelMembers";

export const ChannelPanel = ({ channel }: { channel: StreamChannel }) => (
  <Channel channel={channel}>
    <ChannelHeader />
    <ChannelMembers />
    <MessageList />
    <MessageComposer />
  </Channel>
);

With ChatView, put ChannelMembers among the children of ChannelSlot instead: the slot renders them inside the Channel it opens.

channel.state.members holds the loaded members by user id. Each member carries its user, with name, image, online and last_active. What channels and threads hold lists the rest of channel.state.

Watch the channel with presence

The server sends presence changes only for channels watched with presence. Ask for it where the channel is loaded:

// A channel list: every channel it loads is watched with presence.
const teamChannels = new ChannelPaginator({
  client,
  filters: { members: { $in: [userId] }, type: "team" },
  id: "team",
  requestOptions: { presence: true },
});

// A channel you watch yourself.
const support = await client
  .channel("messaging", "support")
  .watch({ presence: true });

Channel.watch({ presence: true }) sends a request even for a channel a channel list already watched without presence, because the current watch wasn't sent with these options. Later calls with the same options send nothing. Channel list paginators explains requestOptions, and User presence covers presence on the server.

How it works

The list updates by itself.

Info:

Example

When Bob comes online:

  1. The server sends user.presence.changed with Bob's user, because the client watches a channel Bob is in with presence.
  2. The client puts the new user object into Bob's member in channel.state of every channel it holds with him.
  3. channel.state notifies its subscribers, and useChannelMembersState re-renders ChannelMembers with Bob's member.user.online set to true.

A member who joins, leaves or is updated changes the same store, through member.added, member.removed and member.updated, so the list follows those too.