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

Members and online status

This UI cookbook renders the members of the open channel with their online status, in a row of avatars above the message list. The row stays current as members come online, go offline, join or leave.

Best practices

  • Read members from channel.state with useChannelMembersState(channel). It subscribes to the store, so the component re-renders when the members change.
  • Pass useChannelMembersState a pick function, declared at module scope, that returns the member fields you show. The members then update only when one of those fields changes, or when members join or leave.
  • Load the channel with presence, so the server sends presence changes. Without it, 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.
  • Expect only some members in a large channel. A channel loads a limited number of members with its query. The member list of Channel details and members queries all of them.

Implementation

1. The members row

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

src/ChannelMembers.tsx
import { FlatList, StyleSheet, Text, View } from "react-native";
import type { ChannelMemberResponse } from "stream-chat";
import {
  useChannel,
  useChannelMembersState,
  UserAvatar,
  useTheme,
} from "stream-chat-react-native";
import type { ChannelMemberPick } from "stream-chat-react-native";

// The fields the row shows. Other changes to a member don't re-render it.
const pickShownFields: ChannelMemberPick = ({ user }) => ({
  image: user?.image,
  name: user?.name,
  online: user?.online,
});

const keyExtractor = (member: ChannelMemberResponse) =>
  member.user?.id ?? member.user_id ?? "";

const MemberItem = ({ member }: { member: ChannelMemberResponse }) => {
  const {
    theme: { semantics },
  } = useTheme();
  const { user } = member;
  if (!user) return null;

  const name = user.name ?? user.id;

  return (
    <View
      accessible
      accessibilityLabel={`${name}, ${user.online ? "online" : "offline"}`}
      style={styles.item}
    >
      <UserAvatar showOnlineIndicator={user.online} size="lg" user={user} />
      <Text
        numberOfLines={1}
        style={[styles.name, { color: semantics.textSecondary }]}
      >
        {name}
      </Text>
    </View>
  );
};

export const ChannelMembers = () => {
  const channel = useChannel();
  const members = useChannelMembersState(channel, pickShownFields);
  const {
    theme: { semantics },
  } = useTheme();

  return (
    <FlatList
      contentContainerStyle={styles.content}
      data={Object.values(members)}
      horizontal
      keyExtractor={keyExtractor}
      renderItem={({ item }) => <MemberItem member={item} />}
      showsHorizontalScrollIndicator={false}
      style={[styles.list, { borderBottomColor: semantics.borderCoreSubtle }]}
    />
  );
};

const styles = StyleSheet.create({
  content: { gap: 12, paddingHorizontal: 16, paddingVertical: 8 },
  item: { alignItems: "center", gap: 4, width: 56 },
  list: { borderBottomWidth: 1, flexGrow: 0 },
  name: { fontSize: 12 },
});
A row of member avatars with names above the message list, with an online dot on one member on iOS
A row of member avatars with names above the message list, with an online dot on one member on Android

UserAvatar shows the user's image, or their initials, and with showOnlineIndicator the online dot the SDK's own avatars use. It draws no dot for a member who is offline. flexGrow: 0 keeps the horizontal list at the height of its row, so the message list below it takes the rest of the screen.

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. For a status line such as "Last seen 10 minutes ago", useUserActivityStatus(user) returns the text the channel details screen shows.

2. Render it in the channel

Render it inside Channel, above the message list:

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

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

Pass Channel the keyboardVerticalOffset and topInset your screen needs, as Keyboard and insets explains. The row is part of the screen's views, so it changes neither.

Real-time updates

The row updates by itself.

Info:

Example

Cleo, a member of the open channel, comes online:

  1. The server sends user.presence.changed with Cleo's user, because the client loaded a channel she's in with presence.
  2. The client puts the new user object into Cleo's member in channel.state of every channel it holds with her.
  3. channel.state notifies its subscribers. online is one of the picked fields, so useChannelMembersState re-renders ChannelMembers with Cleo online.

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

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

import { ChannelPaginator } from "stream-chat";
import type { StreamChat } from "stream-chat";

export const watchWithPresence = async (client: StreamChat, userId: string) => {
  // 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 });

  return { support, teamChannels };
};

Channel.watch() sends nothing for a channel that's watched and loaded when it gets no options, or the same options as the last Channel.watch() that succeeded during the current watch. A channel list's query isn't such a call, so Channel.watch({ presence: true }) sends a request even for a channel a list already watches. Later calls with the same options send nothing while the watch lasts. Channel list paginators explains requestOptions, and User presence covers presence on the server.

A reconnect ends every watch. Connection recovery queries each registered channel list again with its own requestOptions, so a list with presence: true gets presence back. It reloads an open channel without presence, so for a channel no such list loads, run your Channel.watch({ presence: true }) again on connection.recovered; Connection lists what recovery reloads.

The SDK's member lists

The channel details screen shows members in two places, and they differ from this row:

  • The members section shows the first five members from channel.state.members, through useChannelMembersState, so it follows presence like this row.
  • The full member list, View all, queries every member from the server with a ChannelMemberSearchSource, and searches them by name. It shows each member's status as the query returned it, and queries its first page again only when the member count changes.

Channel details and members explains both, and ChannelMemberItem, the row they render, is an override key.

Read more