Channel list state

This guide describes the behaviour of the channel list, and shows how you can customize it. The channel list is controlled by the ChannelService.

Channel List

On the screenshot you can see the built-in channel list component that integrates with the ChannelService.

Querying channels

The ChannelService will initialize the channel list when you call either of the following methods:

channelService.init(/* see details below */);
channelService.initWithCustomQuery(/* see details below */);

To load more pages:

channelService.loadMoreChannels();

To clear the list:

channelService.reset();

The current state of the channel list and the latest query can be accessed via theese variables:

// Reactive value of the current channel list, you'll be notified when it changes
channelService.channels$.subscribe((channels) => console.log(channels));

// The current value of the channel list
console.log(channelService.channels);

// Reactive value of the latest channel query request, it could be: 'in-progress' | 'success' | 'error'
channelService.channelQueryState$.subscribe((state) => console.log(state));

// Reactive value that tells if there are more pages to load
channelService.hasMoreChannels$.subscribe((hasMoreChannels) =>
  console.log(hasMoreChannels)
);

Built-in queries

The easiest way to initialize the channel list is to use a built-in query, a typical configuration could look like this:

channelService.init({ type: "messaging", members: { $in: ["<user id>"] } });

// If you want, you can add sort configuration and other options
channelService.init(
  { type: "messaging", members: { $in: ["<user id>"] } },
  { name: 1 },
  { limit: 20 }
);

For the full list of capabilities please refer the query channel API documentation.

Custom queries

If built-in quieries aren’t enough for you use-case, you can provide a custom query function that has this signature: (queryType: ChannelQueryType) => Promise<ChannelQueryResult>. ChannelQueryType can be 'first-page' | 'next-page' | 'recover-state', the result is expected to be this format:

{
  channels: Channel[]; // Ordered list of all channels that are displayed
  hasMorePage: boolean; // Are there any more pages to load?
};

Let’s say you’re using the channel invites to add members to a channel. In that case you might want to do a channel list where you display the channels the user is invited to, but not yet joined, at the top. And then all other chanenls, the user already joined. To do this you need to combine two query channel requests. Here is how you can do that:

notJoinedChannelsQuery = new ChannelQuery(
  this.chatService,
  this.channelService,
  {
    type: 'messaging',
    members: { $in: ["<user id>"] },
    joined: false,
  }
);
joinedChannelsQuery = new ChannelQuery(
  this.chatService,
  this.channelService,
  {
    type: 'messaging',
    members: { $in: ["<user id>"] },
    joined: true,
  }
);
areAllNotJoinedChannelsQueried = false;

async myCustomChannelQuery(queryType: ChannelQueryType) {
  if (queryType === 'first-page' || queryType === 'recover-state') {
    this.areAllNotJoinedChannelsQueried = false;
  }

  if (!this.areAllNotJoinedChannelsQueried) {
    const notJoinedQueryResult = await this.notJoinedChannelsQuery.query(
      queryType
    );
    if (notJoinedQueryResult.hasMorePage) {
      return {
        channels: notJoinedQueryResult.channels,
        hasMorePage: notJoinedQueryResult.hasMorePage,
      };
    } else {
      this.areAllNotJoinedChannelsQueried = true;
      const joinedQueryResult = await this.joinedChannelsQuery.query(
        'first-page'
      );
      return {
        channels: [
          ...notJoinedQueryResult.channels,
          ...joinedQueryResult.channels,
        ],
        hasMorePage: joinedQueryResult.hasMorePage,
      };
    }
  } else {
    return this.joinedChannelsQuery.query(queryType);
  }
}

And then provide your query to the ChannelService:

this.channelService.initWithCustomQuery((queryType) =>
  this.myCustomChannelQuery(queryType)
);

The above example used the ChannelQuery class that’s exported by the SDK, but you can use any implementation you like, as long as your custom query follows this method signature: (queryType: ChannelQueryType) => Promise<ChannelQueryResult>. You can reference the ChannelQuery implementation for the details.

Pagination

By default the SDK will use an offset based pagination, where the offset will start from 0, and will be incremented with the number of channels returned from each query request.

However, it’s possible to provide your own pagination logic. Let’s see the below example which sorts the channels alphabetically by their names, and then paginates using the following filter: {name: $gt: <last loaded channel>}

this.channelService.customPaginator = (
  channelQueryResult: Channel<DefaultStreamChatGenerics>[]
) => {
  const lastChannel = channelQueryResult[channelQueryResult.length - 1];
  if (!lastChannel) {
    return undefined;
  } else {
    return {
      type: "filter",
      paginationFilter: {
        name: { $gt: lastChannel.data?.name || "" },
      },
    };
  }
};
this.channelService.init(
  {
    type: "messaging",
    members: { $in: ["<user id>"] },
  },
  { name: 1 }
);

Active channel

The currently selected channel is called the active channel.

// Reactive value of the current active channel, you'll be notified when it changes
channelService.activeChannel$.subscribe((channel) => console.log(channel));

// The current value of the active channel
console.log(channelService.activeChannel);

Here is how you can select/deselect the active channel:

channelService.setAsActiveChannel(<channel to select>);
channelService.deselectActiveChannel();

Selecting a channel as active will immediately mark the channel as read.

By default the SDK will set the first channel as active when initializing the channel list. If you wish to turn off that behvior, set the shouldSetActiveChannel flag to false:

channelService.init(<filter>, <sort>, <options>, false);
channelService.initWithCustomQuery(<custom query>, {shouldSetActiveChannel: false});

WebSocket events

Apart from channel queries, the channel list is also updated on the following WebSocket events:

Event typeDefault behaviorCustom handler to override
channel.deletedRemove channel from the listcustomChannelDeletedHandler
channel.hiddenRemove channel from the listcustomChannelHiddenHandler
channel.truncatedUpdates the channelcustomChannelTruncatedHandler
channel.updatedUpdates the channelcustomChannelUpdatedHandler
channel.visibleAdds the channel to the listcustomChannelVisibleHandler
message.newMoves the channel to top of the listcustomNewMessageHandler
notification.added_to_channelAdds the new channel to the top of the list and starts watching itcustomAddedToChannelNotificationHandler
notification.message_newAdds the new channel to the top of the list and starts watching itcustomNewMessageNotificationHandler
notification.removed_from_channelRemoves the channel from the listcustomRemovedFromChannelNotificationHandler

Our platform documentation covers the topic of channel events in depth.

It’s important to note that filters don’t apply to updates to the list from events. So if you initialize the channel list with this filter:

{
  type: 'messaging',
  members: { $in: ['<user id>'] },
}

And the user receives a message from a team channel, that channel will be added to the channel list by the default notification.message_new handler. If you don’t want that behavior, you will need to provide your custom event handler to all relevant events. Here is an example event handler:

customNewMessageNotificationHandler = async (
  clientEvent: ClientEvent,
  channelListSetter: (channels: Channel<DefaultStreamChatGenerics>[]) => void
) => {
  const channelResponse = clientEvent!.event!.channel!;
  if (channelResponse.type !== "messaging") {
    return;
  }
  const newChanel = this.chatService.chatClient.channel(
    channelResponse.type,
    channelResponse.id
  );
  try {
    // We can only add watched channels to the channel list, so make sure to call `watch`
    await newChanel.watch();
    const existingChannels = this.channelService.channels;
    channelListSetter([newChanel, ...existingChannels]);
  } catch (error) {
    console.error("Failed to watch channel", error);
  }
};

this.channelService.customNewMessageNotificationHandler =
  this.customNewMessageNotificationHandler;
this.channelService.init(/* ... */);

Adding and removing channels

While the SDK doesn’t come with built-in components to create or delete channels it’s easy to create these components should you need them. The SDK even provides a few component hooks to inject you custom UI (channel list, channel preview, channel header). But of course you can also create a completely custom UI.

The channel list will be automatically updated on the notification.added_to_channel and channel.deleted events, however it’s also possible to add and remove channels manually from the list:

// Make sure to watch the channel before adding it
channelService.addChannel(<watched channel>);

channelService.removeChannel('<cid>');
// This will automatically unwatch the chennel, unless you provide the following flag:
channelService.removeChannel('<cid>', false);

It’s important to note that you should make sure that the channel you add is watched, more information about watching channels can be found in our API documentation.

Multiple channel lists

Sometimes we need to show multiple separate channel lists. For example: we want to show 1:1 conversations and team chats in two separate tabs.

Multi Channel List

Here are the necessary steps to achieve this:

  1. Create your own channel list component, and provide a separate ChannelService instance to them using Angular’s dependency injection system:
@Component({
  selector: 'app-custom-channel-list',
  templateUrl: './custom-channel-list.component.html',
  styleUrls: ['./custom-channel-list.component.css'],
  // Each channel list has it's own ChannelService instance
  providers: [ChannelService],
})
  1. Each channel list component will initialize it’s ChannelService instance with the given filter:
this.channelService.init({
  type: "messaging",
  members: { $in: [this.userId] },
  member_count:
    this.channelListType === "1:1 conversations" ? { $lte: 2 } : { $gt: 2 },
});
  1. Each channel list component will have to define custom WebSocket event handlers, where it filters which channel can be added to the list
this.channelService.customAddedToChannelNotificationHandler = (
  clientEvent,
  channelListSetter
) => filter(clientEvent.event.channel, channelListSetter);
this.channelService.customNewMessageNotificationHandler = (
  clientEvent,
  channelListSetter
) => filter(clientEvent.event.channel, channelListSetter);
this.channelService.customChannelVisibleHandler = (
  _,
  channel,
  channelListSetter
) => filter(channel, channelListSetter);

You can checkout the full example on Codesandbox

© Getstream.io, Inc. All Rights Reserved.