This is documentation for Stream Chat Android SDK v5, which is nolonger actively maintained. For up-to-date documentation, see the latest version (v6).

Channels

Channels are where conversations take place between two or more chat users. They contain a list of messages and have a list of the member users that are participating in the conversation.

A channel is identified by its type and id. Some APIs use a cid to identify a channel with a single string - this is the combination of the two pieces of information, as type:id. Almost all API requests are specific to a channel (for example add a message to channel “livestream:rockets”) and as we saw already, channels are organized in groups called channel types. You can read more about channel types here. Channels are unique either by the specified channel id or by the list of members.

Querying Channel List

You can query channels based on built-in fields as well as any custom field you add to channels. Multiple filters can be combined using AND, OR logical operators, each filter can use its comparison (equality, inequality, greater than, greater or equal, etc.). You can find the complete list of supported operators in the query syntax section of the docs.

As an example, let’s say that you want to query the last conversations I participated in sorted by last_message_at.

Here’s an example of how you can query the list of channels:

val request = QueryChannelsRequest(
    filter = Filters.and(
        Filters.eq("type", "messaging"),
        Filters.`in`("members", listOf("thierry")),
    ),
    offset = 0,
    limit = 10,
    querySort = QuerySort.desc("last_message_at")
).apply {
    watch = true
    state = true
}

client.queryChannels(request).enqueue { result ->
    if (result.isSuccess) {
        val channels: List<Channel> = result.data()
    } else {
        // Handle result.error()
    }
}

At a minimum, the filter should include members: { $in: [userID] }.

On messaging and team applications you normally have users added to channels as a member. A good starting point is to use this filter to show the channels the user is participating.

val filter = Filters.`in`("members", listOf("thierry"))

On a support chat, you probably want to attach additional information to channels such as the support agent handling the case and other information regarding the status of the support case (for example open, pending, solved).

val filter = Filters.and(
    Filters.eq("agent_id", user.id),
    Filters.`in`("status", listOf("pending", "open", "new")),
)

Paginating Channels

Query channel requests can be paginated similar to how you paginate on other calls. Here’s a short example:

// Get the first 10 channels
val filter = Filters.`in`("members", "thierry")
val offset = 0
val limit = 10
val request = QueryChannelsRequest(filter, offset, limit)
client.queryChannels(request).enqueue { result ->
    if (result.isSuccess) {
        val channels = result.data()
    } else {
        // Handle result.error()
    }
}

// Get the second 10 channels
val nextRequest = QueryChannelsRequest(
    filter = filter,
    offset = 10, // Skips first 10
    limit = limit
)
client.queryChannels(nextRequest).enqueue { result ->
    if (result.isSuccess) {
        val channels = result.data()
    } else {
        // Handle result.error()
    }
}

Paginating Channel Messages

The channel query endpoint allows you to paginate the list of messages, watchers, and members for one channel. To make sure that you can retrieve a consistent list of messages, pagination does not work with simple offset/limit parameters but instead, it relies on passing the ID of the messages from the previous page.

For example: say that you fetched the first 100 messages from a channel and want to lead the next 100. To do this you need to make a channel query request and pass the ID of the oldest message if you are paginating in descending order or the ID of the newest message if paginating in ascending order.

Use the id_lt parameter to retrieve messages older than the provided ID and id_gt to retrieve messages newer than the provided ID.

The terms id_lt and id_gt stand for ID less than and ID greater than.

ID-based pagination improves performance and prevents issues related to the list of messages changing while you’re paginating. If needed, you can also use the inclusive versions of those two parameters: id_lte and id_gte.

val channelClient = client.channel("messaging", "general")
val pageSize = 10

// Request for the first page
val request = QueryChannelRequest()
    .withMessages(pageSize)

channelClient.query(request).enqueue { result ->
    if (result.isSuccess) {
        val messages: List<Message> = result.data().messages
        if (messages.size < pageSize) {
            // All messages loaded
        } else {
            // Load next page
            val nextRequest = QueryChannelRequest()
                .withMessages(LESS_THAN, messages.last().id, pageSize)
            // ...
        }
    } else {
        // Handle result.error()
    }
}

For members and watchers, we use limit and offset parameters.

The maximum number of messages that can be retrieved at once from the API is 300.

Watching Channels

The call to channel.watch does a few different things in one API call:

  • It creates the channel if it doesn’t exist yet (if this user has the right permissions to create a channel)
  • It queries the channel state and returns members, watchers and messages
  • It watches the channel state and tells the server that you want to receive events when anything in this channel changes

Watching a Channel and Receiving Events

The examples below show how to watch a channel.

val channelClient = client.channel(channelType = "messaging", channelId = "general")

channelClient.watch().enqueue { result ->
    if (result.isSuccess) {
        val channel: Channel = result.data()
    } else {
        // Handle result.error()
    }
}

Watching a channel only works if you have connected as a user to the chat API

The default queryChannels API returns channels and starts watching them. There is no need to also use channel.watch on the channels returned from queryChannels.

val request = QueryChannelsRequest(
    filter = Filters.and(
        Filters.eq("type", "messaging"),
        Filters.`in`("members", listOf(currentUserId)),
    ),
    offset = 0,
    limit = 10,
    querySort = QuerySort.desc("last_message_at")
).apply {
    // Watches the channels automatically
    watch = true
    state = true
}

// Run query on ChatClient
client.queryChannels(request).enqueue { result ->
    if (result.isSuccess) {
        val channels: List<Channel> = result.data()
    } else {
        // Handle result.error()
    }
}

Stop Receiving Channel Events

To stop receiving channel events:

channelClient.stopWatching().enqueue { result ->
    if (result.isSuccess) {
        // Channel unwatched
    } else {
        // Handle result.error()
    }
}

Getting the Watcher Count

To get the watcher count of a channel:

val request = QueryChannelRequest().withState()
channelClient.query(request).enqueue { result ->
    if (result.isSuccess) {
        val channel: Channel = result.data()
        channel.watcherCount
    } else {
        // Handle result.error()
    }
}
val request = QueryChannelRequest()
    .withWatchers(limit = 5, offset = 0)
channelClient.query(request).enqueue { result ->
    if (result.isSuccess) {
        val channel: Channel = result.data()
        val watchers: List<User> = channel.watchers
    } else {
        // Handle result.error()
    }
}

Listening to Watching Events

A user already watching the channel can listen to users starting and stopping watching the channel with the real-time events:

// Start watching channel
channelClient.watch().enqueue {
    /* Handle result */
}

// Subscribe for watching events
channelClient.subscribeFor(
    UserStartWatchingEvent::class,
    UserStopWatchingEvent::class,
) { event ->
    when (event) {
        is UserStartWatchingEvent -> {
            // User who started watching the channel
            val user = event.user
        }
        is UserStopWatchingEvent -> {
            // User who stopped watching the channel
            val user = event.user
        }
    }
}

Creating Channels

Both channel channel.query and channel.watch methods ensure that a channel exists and create one otherwise. If all you need is to ensure that a channel exists, you can use channel.create.

Creating a Channel Using a Channel Id

val channelClient = client.channel(channelType = "messaging", channelId = "general")

channelClient.create().enqueue { result ->
    if (result.isSuccess) {
        val newChannel: Channel = result.data()
    } else {
        // Handle result.error()
    }
}

Creating a Channel for a List of Members

Channels can be used to conversations between users. In most cases, you want conversations to be unique and make sure that a group of users have only a channel.

You can achieve this by leaving the channel ID empty and provide channel type and members. When you do so, the API will ensure that only one channel for the members you specified exists (the order of the members does not matter).

You cannot add/remove members for channels created this way.

client.createChannel(
    channelType = "messaging",
    members = listOf("thierry", "tomasso")
).enqueue { result ->
    if (result.isSuccess) {
        val channel = result.data()
    } else {
        // Handle result.error()
    }
}

Updating Channels

There are two ways to update a channel using the Stream API - a partial or full update. A partial update will retain any custom key-value data, whereas a complete update is going to remove any that are unspecified in the API request.

Partial Update

A partial update can be used to set and unset specific fields when it is necessary to retain additional custom data fields on the object. AKA a patch style update.

// Here's a channel with some custom field data that might be useful
val channelClient = client.channel(channelType = "messaging", channelId = "general")

channelClient.create(
    members = listOf("thierry", "tomasso"),
    extraData = mapOf(
        "source" to "user",
        "source_detail" to mapOf("user_id" to 123),
        "channel_detail" to mapOf(
            "topic" to "Plants and Animals",
            "rating" to "pg"
        )
    )
).execute()

// let's change the source of this channel
channelClient.updatePartial(set = mapOf("source" to "system")).execute()

// since it's system generated we no longer need source_detail
channelClient.updatePartial(unset = listOf("source_detail")).execute()

// and finally update one of the nested fields in the channel_detail
channelClient.updatePartial(set = mapOf("channel_detail.topic" to "Nature")).execute()

// and maybe we decide we no longer need a rating
channelClient.updatePartial(unset = listOf("channel_detail.rating")).execute()

Full Update

The updateChannel function updates all of the channel data. Any data that is present on the channel and not included in a full update will be deleted.

val channelClient = client.channel("messaging", "general")

channelClient.update(
    message = Message(
        text = "Thierry changed the channel color to green"
    ),
    extraData = mapOf(
        "name" to "myspecialchannel",
        "color" to "green",
    ),
).enqueue { result ->
    if (result.isSuccess) {
        val channel = result.data()
    } else {
        // Handle result.error()
    }
}

Channel Members

Querying Members

Sometimes channels will have many hundreds (or thousands) of members and it is important to be able to access ID’s and information on all of these members. The queryMembers endpoint queries the channel members and allows the user to paginate through a full list of users in channels with very large member counts. The endpoint supports filtering on numerous criteria to efficiently return member information.

The members are sorted by created_at in ascending order.

Stream Chat does not run MongoDB on the backend, only a subset of the query options are available.

Here’s some example of how you can query the list of members:

val channelClient = client.channel("messaging", "general")

val offset = 0 // Use this value for pagination
val limit = 10
val sort = QuerySort<Member>()

// Channel members can be queried with various filters
// 1. Create the filter, e.g query members by user name
val filterByName = Filters.eq("name", "tommaso")
// 2. Call queryMembers with that filter
channelClient.queryMembers(offset, limit, filterByName, sort).enqueue { result ->
    if (result.isSuccess) {
        val members: List<Member> = result.data()
    } else {
        Log.e(TAG, String.format("There was an error %s", result.error()), result.error().cause)
    }
}

// Here are some other commons filters you can use:

// Autocomplete members by user name (names containing "tom")
val filterByAutoCompleteName = Filters.autocomplete("name", "tom")

// Query member by id
val filterById = Filters.eq("id", "tommaso")

// Query multiple members by id
val filterByIds = Filters.`in`("id", listOf("tommaso", "thierry"))

// Query channel moderators
val filterByModerator = Filters.eq("is_moderator", true)

// Query for banned members in channel
val filterByBannedMembers = Filters.eq("banned", true)

// Query members with pending invites
val filterByPendingInvite = Filters.eq("invite", "pending")

// Query all the members
val filterByNone = FilterObject()

// Results can also be ordered with the _QuerySort_ param
// For example, this will order results by member creation time, descending
val createdAtDescendingSort = QuerySort<Member>().desc("created_at")

Adding Members to a Channel

Using the addMembers() method adds the given users as members:

val channelClient = client.channel("messaging", "general")

// Add members with ids "thierry" and "josh"
channelClient.addMembers(listOf("thierry", "josh")).enqueue { result ->
    if (result.isSuccess) {
        val channel: Channel = result.data()
    } else {
        // Handle result.error()
    }
}

Removing Members from a Channel

Using the removeMembers() method removes the given users from members:

val channelClient = client.channel("messaging", "general")

// Remove member with id "tommaso"
channelClient.removeMembers(listOf("tommaso")).enqueue { result ->
    if (result.isSuccess) {
        val channel: Channel = result.data()
    } else {
        // Handle result.error()
    }
}

Channel Invites

Stream Chat provides the ability to invite users to a channel via the channel method with the invites array. Upon invitation, the end-user will receive a notification that they were invited to the specified channel.

Inviting a User

See the following for an example on how to invite a user to a new channel by adding an invites array containing the user ID:

val channelClient = client.channel("messaging", "general") 
val data = mapOf( 
    "members" to listOf("thierry", "tommaso"), 
    "invites" to listOf("nick") 
) 
 
channelClient.create(data).enqueue { result -> 
    if (result.isSuccess) { 
        val channel = result.data() 
    } else { 
        // Handle result.error() 
    } 
}

To invite users to an existing channel, you can use the following snippet:

val channelClient = client.channel("messaging", "general")

channelClient.inviteMembers(listOf("thierry", "josh")).enqueue { result ->
    if (result.isSuccess) {
        val channel = result.data()
    } else {
        // Handle result.error()
    }
}

Accepting an Invite

In order to accept an invite, you must use call the acceptInvite method. The acceptInvite method accepts and object with an optional message property. Please see below for an example of how to call acceptInvite:

channelClient.acceptInvite( 
    message = "Nick joined this channel!" 
).enqueue { result -> 
    if (result.isSuccess) { 
        val channel = result.data() 
    } else { 
        // Handle result.error() 
    } 
}

Rejecting an Invite

To reject an invite, call the rejectInvite method. This method does not require a user ID as it pulls the user ID from the current session in store from the connectUser call.

channelClient.rejectInvite().enqueue { result ->  
    if (result.isSuccess) {  
        // Invite rejected  
    } else {  
        // Handle result.error()  
    }  
}

Querying for Accepted Invites

Querying for accepted invites is done via the queryChannels method. This allows you to return a list of accepted invites with a single call. See below for an example:

val request = QueryChannelsRequest( 
    filter = Filters.eq("invite", "accepted"), 
    offset = 0, 
    limit = 10 
) 
client.queryChannels(request).enqueue { result -> 
    if (result.isSuccess) { 
        val channels: List<Channel> = result.data() 
    } else { 
        // Handle result.error() 
    } 
}

Querying for Rejected Invites

Similar to querying for accepted invites, you can query for rejected invites with queryChannels. See below for an example:

val request = QueryChannelsRequest( 
    filter = Filters.eq("invite", "rejected"), 
    offset = 0, 
    limit = 10 
) 
client.queryChannels(request).enqueue { result -> 
    if (result.isSuccess) { 
        val channels: List<Channel> = result.data() 
    } else { 
        // Handle result.error() 
    } 
}

Muting Channels

Messages added to a channel will not trigger push notifications, nor change the unread count for the users that muted it.

By default, mutes stay in place indefinitely until the user removes it; however, you can optionally set an expiration time.

Muting a Channel

client.muteChannel(channelType, channelId)
    .enqueue { result: Result<Unit> ->
        if (result.isSuccess) {
            // Channel muted
        } else {
            // Handle result.error()
        }
    }

Muting a Channel with Expiration Time

// Mute a channel for 60 minutes
client.muteChannel(channelType, channelId, expiration = 60 * 60 * 1000)
    .enqueue { result: Result<Unit> ->
        if (result.isSuccess) {
            // Channel muted
        } else {
            // Handle result.error() 
        }
    }

Retrieving Muted Channels

The list of muted channels and their expiration time is returned when the user connects.

// get list of muted channels when user is connected
client.connectUser(user, "user-token", object : InitConnectionListener() {
    override fun onSuccess(data: ConnectionData) {
        val user = data.user
        // mutes contains the list of channel mutes
        val mutes: List<ChannelMute> = user.channelMutes
    }
})

// get updates about muted channels
client
    .events()
    .subscribe { event: ChatEvent? ->
        if (event is NotificationChannelMutesUpdated) {
            val mutes = event.me.channelMutes
        } else if (event is NotificationMutesUpdated) {
            val mutes = event.me.channelMutes
        }
    }

Messages added to muted channels do not increase the unread messages count.

Muted channels can be filtered or excluded by using the muted in your query channels filter.

// Filter for all channels excluding muted ones
val notMutedFilter = Filters.and(
    Filters.eq("muted", false),
    Filters.`in`("members", listOf(currentUserId)),
)

// Filter for muted channels
val mutedFilter = Filters.eq("muted", true)

// Executing a channels query with either of the filters
client.queryChannels(QueryChannelsRequest(
    filter = filter, // Set the correct filter here
    offset = 0,
    limit = 10,
)).enqueue { result ->
    if (result.isSuccess) {
        val channels: List<Channel> = result.data()
    } else {
        // Handle result.error()
    }
}

Un-muting a Channel

// Unmute channel for current user
channelClient.unmute().enqueue { result ->
    if (result.isSuccess) {
        // Channel is unmuted
    } else {
        // Handle result.error()
    }
}

Checking If User Muted a Channel

There’s a utility method to check if a user muted a Channel.

val isMuted = channel.isMutedFor(user) 

if (isMuted) {
  // Handle UI for muted channel
} else {
  // Handle UI for not muted channel
}

Deleting and Hiding Channels

Deleting a Channel

You can delete a Channel using the delete method. This marks the channel as deleted and hides all the content.

val channelClient = client.channel("messaging", "general")

channelClient.delete().enqueue { result ->
    if (result.isSuccess) {
        val channel = result.data()
    } else {
        // Handle result.error()
    }
}

Hiding a Channel

Hiding a channel will remove it from query channel requests for that user until a new message is added. Please keep in mind that hiding a channel is only available to members of that channel.

Optionally you can also clear the entire message history of that channel for the user. This way, when a new message is received, it will be the only one present in the channel.

// Hides the channel until a new message is added there
channelClient.hide().enqueue { result ->
    if (result.isSuccess) {
        // Channel is hidden
    } else {
        // Handle result.error()
    }
}

// Shows a previously hidden channel
channelClient.show().enqueue { result ->
    if (result.isSuccess) {
        // Channel is shown
    } else {
        // Handle result.error()
    }
}

// Hide the channel and clear the message history
channelClient.hide(clearHistory = true).enqueue { result ->
    if (result.isSuccess) {
        // Channel is hidden
    } else {
        // Handle result.error()
    }
}

Truncating a Channel

Messages from a channel can be truncated. This removes all of the messages but doesn’t affect the channel data or members.

// Truncate the channel
channelClient.truncate().enqueue { result ->
    if (result.isSuccess) {
        // Channel is truncated
    } else {
        // Handle result.error()
    }
}

Slow Mode

Slow mode helps reduce noise on a channel by limiting users to a maximum of 1 message per cooldown interval.

Enabling Slow Mode

Slow mode is disabled by default and can be enabled/disabled by admins and moderators.

val channelClient = client.channel("messaging", "general")

// Enable slow mode and set cooldown to 1s
channelClient.enableSlowMode(cooldownTimeInSeconds = 1).enqueue { /* Result handling */ }

// Increase cooldown to 30s
channelClient.enableSlowMode(cooldownTimeInSeconds = 30).enqueue { /* Result handling */ }

// Disable slow mode
channelClient.disableSlowMode().enqueue { /* Result handling */ }

When a user posts a message during the cooldown period, the API returns an error message.

Retrieving Cooldown Value

You can avoid hitting the APIs and instead show such limitation on the send message UI directly. When slow mode is enabled, channels include a cooldown field containing the current cooldown period in seconds.

val channelClient = client.channel("messaging", "general")

// Get the cooldown value
channelClient.query(QueryChannelRequest()).enqueue { result ->
    if (result.isSuccess) {
        val channel = result.data()
        val cooldown = channel.cooldown

        val message = Message(text = "Hello")
        channelClient.sendMessage(message).enqueue {
            // After sending a message, block the UI temporarily
            // The disable/enable UI methods have to be implemented by you
            disableMessageSendingUi()

            Handler(Looper.getMainLooper())
                .postDelayed(::enableMessageSendingUi, cooldown.toLong())
        }
    }
}
© Getstream.io, Inc. All Rights Reserved.