Skip to content

Client Cookbook

Rafal Adasiewicz edited this page Apr 28, 2021 · 66 revisions

This cheat sheet provides additional information to the official StreamChat SDK documentation on our website. You can find here more detailed information, richer code snippets, and commentary on the provided solutions.

Summary


Didn't find what you were looking for? Open an issue in our repo and suggest a new topic!


Overview

Chat Client or LLC (low-level client) is a low-level client for making API calls and receiving chat events. This library integrates directly with Stream Chat APIs and does not include state handling or UI. This library supports both Kotlin and Java usage, but we strongly recommend using Kotlin.

Setup

Adding Dependencies

Update your repositories in the project level build.gradle file:

allprojects {
    repositories {
        mavenCentral()
    }
}

Open up the app module's build.gradle script and make the following changes:

android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation "io.getstream:stream-chat-android-client:$stream_version"
}

For the latest version, check the Releases page.

Initializing SDK

As a first step, you need to initialize ChatClient, which is the main entry point for all operations in the library. You should only create the client once and re-use it across your application. Typically ChatClient is initialized in Application class:

class App : Application() {

    override fun onCreate() {
        super.onCreate()
        val client = ChatClient.Builder("apiKey", context).build()

        // Static reference to initialised client 
        val staticClientRef = ChatClient.instance()
    }
}

With this, you will be able to retrieve instances of the different components from any part of your application using instance(). Here's an example:

class MainActivity: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val chatClient = ChatClient.instance()
    }
}

Connecting User

The next step is connecting the user. A valid StreamChat token is all you need to properly set your current app user as the current user of ChatClient. This token can't be created locally and it must be provided by your backend.

val user = User(
    id = "bender",
    extraData = mutableMapOf(
        "name" to "Bender",
        "image" to "https://bit.ly/321RmWb",
    ),
)

ChatClient.instance().connectUser(user = user, token = "userToken")
    .enqueue { result ->
        if (result.isSuccess) {
            // Handle success
        } else {
            // Handle error
    }
}

Calling SDK methods

Most SDK methods return a Call object, which is a pending operation waiting to be executed.

Running calls synchronously

You can run a Call synchronously, in a blocking way, using the execute method:

// Only call this from a background thread
val messageResult = channelClient.sendMessage(message).execute()

Running calls asynchronously

You can run a Call asynchronously, on a background thread, using the enqueue method. The callback passed to enqueue will be called on the UI thread.

// Safe to call from the main thread
channelClient.sendMessage(message).enqueue { result: Result<Message> ->
    if (result.isSuccess) {
        val sentMessage = result.data()
    } else {
        // Handle result.error()
    }
}

If you are using Kotlin coroutines, you can also await() the result of a Call in a suspending way:

viewModelScope.launch {
    // Safe to call from any CoroutineContext
    val messageResult = channelClient.sendMessage(message).await()
}

Error handling

Actions defined in a Call return Result objects. These contain either the result of a successful operation or the error that caused the operation to fail.

You can check whether a Result is successful or an error:

// Exactly one of these will be true for each Result
result.isSuccess
result.isError

If the result was successful, you can get the contained data with data(). Otherwise, you can read error() and handle it appropriately.

if (result.isSuccess) {
    // Use result.data()
} else {
    // Handle result.error()
}

Calling data() on a failed Result or calling error() on a successful Result will throw an IllegalStateException.

User Types

Connecting with a Regular User

val user = User( 
    id = "bender", 
    extraData = mutableMapOf( 
        "name" to "Bender", 
        "image" to "https://bit.ly/321RmWb", 
    ), 
) 
 
// You can set up a user token in two ways: 
 
// 1. Setup the current user with a JWT token 
val token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZmFuY3ktbW9kZS0wIn0.rSnrWOv8EbsiYzJlvVwqwCgATZ1Magj_fZl-bZyCHKI" 
client.connectUser(user, token).enqueue { result -> 
    if (result.isSuccess) { 
        // Logged in 
        val user: User = result.data().user 
        val connectionId: String = result.data().connectionId 
    } else { 
        // Handle result.error() 
    } 
} 
 
// 2. Setup the current user with a TokenProvider 
val tokenProvider = object : TokenProvider { 
    // Make a request to your backend to generate a valid token for the user 
    override fun loadToken(): String = yourTokenService.getToken(user) 
} 
client.connectUser(user, tokenProvider).enqueue { /* ... */ }

Connecting with a Guest User

Guest sessions can be created client-side and do not require any server-side authentication. Support and livestreams are common use cases for guest users because often you want a visitor to be able to use chat on your application without (or before) they have a regular user account.

Guest users are not available to application using multi-tenancy (teams).

Unlike anonymous users, guest users are counted towards your MAU usage.

Guest users have a limited set of permissions. You can create a guest user session by using connectGuestUser instead of connectUser.

client.connectGuestUser(userId = "bender", username = "Bender").enqueue { /*... */ }

Connecting with an Anonymous User

If a user is not logged in, you can call the connectAnonymousUser method. While you’re anonymous, you can’t do much, but for the livestream channel type, you’re still allowed to read the chat conversation.

client.connectAnonymousUser().enqueue { /*... */ }

When you connect to chat using anonymously you receive a special user back with the following data:

{ 
	"id": "!anon", 
	"role": "anonymous", 
	"roles": [], 
	"created_at": "0001-01-01T00:00:00Z", 
	"updated_at": "0001-01-01T00:00:00Z", 
	"last_active": "2020-11-02T18:36:01.125136Z", 
	"banned": false, 
	"online": true, 
	"invisible": false, 
	"devices": [], 
	"mutes": [], 
	"channel_mutes": [], 
	"unread_count": 0, 
	"total_unread_count": 0, 
	"unread_channels": 0, 
	"language": "" 
}

Anonymous users are not counted toward your MAU number and only have an impact on the number of concurrent connected clients.

Querying Users

Querying Regular Users

The Query Users method allows you to search for users and see if they are online/offline. The example below shows how you can retrieve the details for 3 users in one API call:

// Search for users with id "john", "jack", or "jessie" 
val request = QueryUsersRequest( 
    filter = Filters.`in`("id", listOf("john", "jack", "jessie")), 
    offset = 0, 
    limit = 3, 
) 
 
client.queryUsers(request).enqueue { result -> 
    if (result.isSuccess) { 
        val users: List<User> = result.data() 
    } else { 
        // Handle result.error() 
    } 
}

Querying Banned Users

Another option is to query for banned users. This can be done with the following code snippet:

val request = QueryUsersRequest( 
    filter = Filters.eq("banned", true), 
    offset = 0, 
    limit = 10, 
) 
 
client.queryUsers(request).enqueue { /* ... */ }

Please be aware that this query will return users banned across the entire app, not at a channel level.

You can filter and sort on the custom fields you've set for your user, the user id, and when the user was last active.

The options for the queryUser method are presence, limit, and offset. If presence is true this makes sure you receive the user.presence.changed event when a user goes online or offline.

Querying Users by Search Term

You can autocomplete the results of your user query by username and/or ID.

If you want to return all users whose username includes 'ro', you could do so with the following:

al request = QueryUsersRequest( 
    filter = Filters.autocomplete("name", "ro"), 
    offset = 0, 
    limit = 10, 
) 
 
client.queryUsers(request).enqueue { /* ... */ }

This would return an array of any matching users, such as:

[ 
    { 
        "id": "userID", 
        "name": "Curiosity Rover" 
    }, 
    { 
        "id": "userID2", 
        "name": "Roxy" 
    }, 
    { 
        "id": "userID3", 
        "name": "Roxanne" 
    } 
]

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() 
    } 
}

Watching a Channel

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. Note that you need to be connected as a user before you can 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 
        } 
    } 
}

Querying Channels

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 (ie. 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.

Updating a Channel

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() 
    } 
}

Changing Channel Members

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("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("tommaso").enqueue { result -> 
    if (result.isSuccess) { 
        val channel: Channel = result.data() 
    } else { 
        // Handle result.error() 
    } 
}

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 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() 
    } 
}

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() 
    } 
}

Deleting and Hiding a Channel

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() 
    } 
}

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 is muted 
        } else { 
            result.error().printStackTrace() 
        } 
    } 

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() 
    } 
}

Unmuting a Channel

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

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 orderd with the QuerySort param 
// For example, this will order results by member creation time, descending 
val createdAtDescendingSort = QuerySort<Member>().desc("created_at")

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()) 
        } 
    } 
}

Messages

Sending a Message

You can send a simple message using the sendMessage call:

val channelClient = client.channel("messaging", "general") 
val message = Message( text = "Sample message text" ) 
 
channelClient.sendMessage(message).enqueue { result -> 
    if (result.isSuccess) { 
        val sentMessage: Message = result.data() 
    } else { 
        // Handle result.error() 
    } 
}

Sending a Message with Attachment

You can send a message with an attachment using the sendMessage call:

// Create an image attachment 
val attachment = Attachment( 
    type = "image", 
    imageUrl = "https://bit.ly/2K74TaG", 
    thumbUrl = "https://bit.ly/2Uumxti", 
    extraData = mutableMapOf("myCustomField" to 123), 
) 
 
// Create a message with the attachment and a user mention 
val message = Message( 
    text = "@Josh I told them I was pesca-pescatarian. Which is one who eats solely fish who eat other fish.", 
    attachments = mutableListOf(attachment), 
    mentionedUsersIds = mutableListOf("josh-id"), 
    extraData = mutableMapOf("anotherCustomField" to 234), 
) 
 
// Send the message to the channel 
channelClient.sendMessage(message).enqueue { /* ... */ }

Getting a Message

You can get a single message by its ID using the getMessage call:

channelClient.getMessage("message-id").enqueue { result -> 
    if (result.isSuccess) { 
        val message = result.data() 
    } else { 
        // Handle result.error() 
    } 
}

Updating a Message

You can edit a message by calling updateMessage and including a message with an ID:

// Update some field of the message 
message.text = "my updated text" 
 
// Send the message to the channel 
channelClient.updateMessage(message).enqueue { result -> 
    if (result.isSuccess) { 
        val updatedMessage = result.data() 
    } else { 
        // Handle result.error() 
    } 
}

Deleting a Message

You can delete a message by calling deleteMessage and including a message with an ID:

channelClient.deleteMessage("message-id").enqueue { result -> 
    if (result.isSuccess) { 
        val deletedMessage = result.data() 
    } else { 
        // Handle result.error() 
    } 
}

File Uploads

The channel.sendImage and channel.sendFile methods make it easy to upload files.

This functionality defaults to using the Stream CDN. If you would like, you can easily change the logic to upload to your own CDN of choice.

The maximum file size is 20mb for the Stream Chat CDN.

Uploading an Image

val channelClient = client.channel("messaging", "general") 
 
// Upload an image without detailed progress 
channelClient.sendImage(imageFile).enqueue { result-> 
    if (result.isSuccess) { 
        // Successful upload, you can now attach this image 
        // to an message that you then send to a channel 
        val imageUrl = result.data() 
        val attachment = Attachment( 
            type = "image", 
            imageUrl = imageUrl, 
        ) 
        val message = Message( 
            attachments = mutableListOf(attachment), 
        ) 
        channelClient.sendMessage(message).enqueue { /* ... */ } 
    } 
} 

In the code example above, note how the message attachments are created after the files are uploaded.

Uploading a File

// Upload a file, monitoring for progress with a ProgressCallback 
channelClient.sendFile(anyOtherFile, object : ProgressCallback { 
    override fun onSuccess(file: String) { 
        val fileUrl = file 
    } 
 
    override fun onError(error: ChatError) { 
        // Handle error 
    } 
 
    override fun onProgress(progress: Long) { 
        // You can render the uploading progress here 
    } 
}).enqueue() // No callback passed to enqueue, as we'll get notified above anyway

Customizing Upload Logic

You can use your own CDN. You'll have to create your own implementation of the FileUploader interface, and any upload and delete calls will be sent to that implementation.

The code examples below show how to change where files are uploaded:

// Set a custom FileUploader implementation when building your client 
val client = ChatClient.Builder("39mr6a3z4tem", context) 
    .fileUploader(MyFileUploader()) 
    .build() 
}

Reactions

Stream Chat has built-in support for user Reactions. Common examples are likes, comments, loves, etc.

Sending a Reaction

val channelClient = client.channel("messaging", "general") 
 
val reaction = Reaction( 
    messageId = "message-id", 
    type = "like", 
    score = 1
) 
channelClient.sendReaction(reaction).enqueue { result -> 
    if (result.isSuccess) { 
        val sentReaction: Reaction = result.data() 
    } else { 
        // Handle result.error() 
    } 
} 

Replacing a Reaction

Add reaction 'like' and replace all other reactions of this user by it:

channelClient.sendReaction(reaction, enforceUnique = true).enqueue { result -> 
    if (result.isSuccess) { 
        val sentReaction = result.data() 
    } else { 
        // Handle result.error() 
    } 
}

Deleting a Reaction

channelClient.deleteReaction( 
    messageId = "message-id", 
    reactionType = "like", 
).enqueue { result -> 
    if (result.isSuccess) { 
        val message = result.data() 
    } else { 
        // Handle result.error() 
    } 
}

Paginating Reactions

Messages returned by the APIs automatically include the 10 most recent reactions. You can also retrieve more reactions and paginate using the following logic:

// Get the first 10 reactions 
channelClient.getReactions( 
    messageId = "message-id", 
    offset = 0, 
    limit = 10, 
).enqueue { result -> 
    if (result.isSuccess) { 
        val reactions: List<Reaction> = result.data() 
    } else { 
        // Handle result.error() 
    } 
} 
 
// Get the second 10 reactions 
channelClient.getReactions( 
    messageId = "message-id", 
    offset = 10, 
    limit = 10, 
).enqueue { /* ... */ } 
 
// Get 10 reactions after particular reaction 
channelClient.getReactions( 
    messageId = "message-id", 
    firstReactionId = "reaction-id", 
    limit = 10, 
).enqueue { /* ... */ }

Sending a Clap Reaction

You can use the Reactions API to build something similar to Medium's clap reactions. If you are not familiar with this, Medium allows you to clap articles more than once and shows the sum of all claps from all users.

val reaction = Reaction(messageId = "message-id", type = "clap", score = 5) 
channelClient.sendReaction(reaction).enqueue { /* ... */ }

Thread and Replies

Threads can be very helpful to keep the conversation organized and reduce noise.

Creating a Thread

To create a thread you simply send a message with a parent_id. Have a look at the example below:

val message = Message( 
    text = "Hello there!", 
    parentId = parentMessage.id, 
) 
 
// Send the message to the channel 
channelClient.sendMessage(message).enqueue { result -> 
    if (result.isSuccess) { 
        val sentMessage = result.data() 
    } else { 
        // Handle result.error() 
    } 
}

If you specify show_in_channel, the message will be visible both in a thread of replies as well as the main channel.

Messages inside a thread can also have reactions, attachments and mention as any other message.

Retrieving Thread Messages

When you read a channel you do not receive messages inside threads. The parent message includes the count of replies which it is usually what apps show as the link to the thread screen. Reading a thread and paginating its messages works in a very similar way as paginating a channel.

// Retrieve the first 20 messages inside the thread 
client.getReplies(parentMessage.id, limit = 20).enqueue { result -> 
    if (result.isSuccess) { 
        val replies: List<Message> = result.data() 
    } else { 
        // Handle result.error() 
    } 
} 
 
// Retrieve the 20 more messages before the message with id "42" 
client.getRepliesMore( 
    messageId = parentMessage.id, 
    firstId = "42", 
    limit = 20, 
).enqueue { /* ... */ }

Quoting a Message

Instead of replying in a thread, it's also possible to quote a message. Quoting a message doesn't result in the creation of a thread; the message is quoted inline.

To quote a message, simply provide the quoted_message_id field when sending a message:

val message = Message( 
    text = "This message quotes another message!", 
    replyMessageId = originalMessage.id, 
) 
channelClient.sendMessage(message).enqueue { /* ... */ }

Based on the provided quoted_message_id, the quoted_message field is automatically enriched when querying channels with messages. Example response:

{ 
    "id": "message_with_quoted_message", 
    "text": "This is the first message that quotes another message", 
    "quoted_message_id": "first_message_id", 
    "quoted_message": {  
        "id": "first_message_id",  
        "text": "The initial message" 
    } 
}

Silent Messages

Silent messages are special messages that don't increase the unread messages count nor mark a channel as unread.

Sending a Silent Message

Creating a silent message is very simple, you only need to include the silent field boolean field and set it to true.

val message = Message( 
    text = "You and Jane are now matched!", 
    user = systemUser, 
    silent = true, 
) 
channelClient.sendMessage(message).enqueue { /* ... */ }

Existing messages cannot be turned into a silent message or vice versa. Silent messages do send push notifications

Search

Message search is built-in to the chat API. You can enable and/or disable the search indexing on a per-channel type.

Searching for Messages

The command shown below selects the channels in which John is a member. Next, it searches the messages in those channels for the keyword “'supercalifragilisticexpialidocious'”:

client.searchMessages( 
    SearchMessagesRequest( 
        offset = 0, 
        limit = 10, 
        channelFilter = Filters.`in`("members", listOf("john")), 
        messageFilter = Filters.autocomplete("text", "supercalifragilisticexpialidocious") 
    ) 
).enqueue { result -> 
    if (result.isSuccess) { 
        val messages: List<Message> = result.data() 
    } else { 
        // Handle result.error() 
    } 
}

Pagination works via the standard limit and offset parameters. The first argument, filter, uses a MongoDB style query expression.

Searching for Messages with Attachments

Additionally, this endpoint can be used to search for messages that have attachments.

channelClient.getMessagesWithAttachments( 
    offset = 0, 
    limit = 10, 
    type = "image", 
).enqueue { result -> 
    if (result.isSuccess) { 
        // These messages will contain at least one of the desired  
        // type of attachment, but not necessarily all of their  
        // attachments will have the specified type 
        val messages: List<Message> = result.data() 
    } 
}

Pinned Messages

Pinned messages allow users to highlight important messages, make announcements, or temporarily promote content. Pinning a message is, by default, restricted to certain user roles, but this is flexible. Each channel can have multiple pinned messages and these can be created or updated with or without an expiration.

Pinning a Message

An existing message can be updated to be pinned or unpinned by using the channel.pinMessage and channel.unpinMessage methods. Or a new message can be pinned when it is sent by setting the pinned and pin_expires fields when using channel.sendMessage.

// Create pinned message 
val pinExpirationDate = Calendar.getInstance().apply { set(2077, 1, 1) }.time 
val message = Message( 
    text = "Hey punk", 
    pinned = true, 
    pinExpires = pinExpirationDate 
) 
 
channelClient.sendMessage(message).enqueue { /* ... */ } 
 
// Unpin message 
channelClient.unpinMessage(message).enqueue { /* ... */ } 
 
// Pin message for 120 seconds 
channelClient.pinMessage(message, timeout = 120).enqueue { /* ... */ } 
 
// Change message expiration to 2077 
channelClient.pinMessage(message, expirationDate = pinExpirationDate).enqueue { /* ... */ } 
 
// Remove expiration date from pinned message 
channelClient.pinMessage(message, expirationDate = null).enqueue { /* ... */ }

To pin the message user has to have PinMessage permission.

Retrieving Pinned Messages

You can easily retrieve the last 10 pinned messages from the channel.pinned_messages field:

channelClient.query(QueryChannelRequest()).enqueue { result -> 
    if (result.isSuccess) { 
        val pinnedMessages: List<Message> = result.data().pinnedMessages 
    } else { 
        // Handle result.error() 
    } 
}

Searching for Pinned Messages

Stream Chat also provides a search filter in case if you need to display more than 10 pinned messages in a specific channel.

val request = SearchMessagesRequest( 
    offset = 0, 
    limit = 30, 
    channelFilter = Filters.`in`("cid", "channelType:channelId"), 
    messageFilter = Filters.eq("pinned", true) 
) 
 
client.searchMessages(request).enqueue { result -> 
    if (result.isSuccess) { 
        val pinnedMessages = result.data() 
    } else { 
        // Handle result.error() 
    } 
}

Moderation Tools

Flagging a Message

Any user is allowed to flag a message.

client.flagMessage("message-id").enqueue { result -> 
    if (result.isSuccess) { 
        // Message was flagged 
        val flag: Flag = result.data() 
    } else { 
        // Handle result.error() 
    } 
} 
 
client.flagUser("user-id").enqueue { result -> 
    if (result.isSuccess) { 
        // User was flagged 
        val flag: Flag = result.data() 
    } else { 
        // Handle result.error() 
    } 
}

Muting a User

Any user is allowed to mute another user. Mutes are stored at the user level and returned with the rest of the user information when connectUser is called. A user will be muted until the user is unmuted or the mute is expired.

client.muteUser("user-id").enqueue { result -> 
    if (result.isSuccess) { 
        // User was muted 
        val mute: Mute = result.data() 
    } else { 
        // Handle result.error() 
    } 
} 
 
client.unmuteUser("user-id").enqueue { result -> 
    if (result.isSuccess) { 
        // User was unmuted 
    } else { 
        // Handle result.error() 
    } 
}

After muting a user messages will still be delivered via web-socket. Implementing business logic such as hiding messages from muted users or display them differently is left to the developer to implement.

Messages from muted users are not delivered via push (APN/Firebase)

Banning a User

Users can be banned from an app entirely or from a channel. When a user is banned, it will not be allowed to post messages until the ban is removed or expired but it will be able to connect to Chat and to channels as before.

In most cases, only admins or moderators are allowed to ban other users from a channel.

Banning a user from all channels can only be done using server-side auth.

// Ban user for 60 minutes from a channel 
channelClient.banUser(targetId = "user-id", reason = "Bad words", timeout = 60).enqueue { result -> 
    if (result.isSuccess) { 
        // User was banned 
    } else { 
        // Handle result.error() 
    } 
} 
 
channelClient.unBanUser(targetId = "user-id").enqueue { result -> 
    if (result.isSuccess) { 
        // User was unbanned 
    } else { 
        // Handle result.error() 
    } 
}

Retrieving Banned Users

You can list banned users from the API directly using the user search endpoint or the member search endpoint. You use the first to get the list of users banned from all channels and the second to list users that are banned from a specific channel. Both endpoints support additional query parameters as well as pagination, you can find the full information on the specific doc sections.

// retrieve the list of banned users 
client.queryUsers( 
    QueryUsersRequest( 
        filter = Filters.eq("banned", true), 
        offset = 0, 
        limit = 10, 
    ) 
).enqueue { result -> 
    if (result.isSuccess) { 
       val users: List<User> = result.data() 
   } else { 
       // Handle result.error() 
   } 
} 
 
// query for banned members in channel 
channelClient.queryMembers( 
    offset = 0, 
    limit = 10, 
    filter = Filters.eq("banned", true), 
    sort = QuerySort(), 
    members = emptyList() 
).enqueue { result -> 
    if (result.isSuccess) { 
        val members: List<Member> = result.data() 
    } else { 
        // Handle result.error() 
    } 
}

Retrieving Banned Users From Specific Channels

You can list banned users from a specific channel using the query banned users endpoint which allows you to get paginated results:

// Get the bans for channel livestream:123 in descending order
channelClient.queryBannedUsers(
    sort = QuerySort.desc(BannedUsersSort::createdAt),
).enqueue { result ->
    if (result.isSuccess) {
        val bannedUsers: List<BannedUser> = result.data()
    } else {
        // Handle result.error()
   }
}

// Get the page of bans which where created before or equal date for the same channel
client.queryBannedUsers(
    filter = Filters.eq("channel_cid", "livestream:123"),
    sort = QuerySort.desc(BannedUsersSort::createdAt),
    createdAtBeforeOrEqual = Date(),
).enqueue { result ->
    if (result.isSuccess) {
        val bannedUsers: List<BannedUser> = result.data()
    } else {
        // Handle result.error()
    }
}

You can also use in filter to query banned users from multiple channels:

client.queryBannedUsers(
    filter = Filters.`in`("channel_cid", listOf("livestream:123", "livestream:456")),
    sort = QuerySort.desc(BannedUsersSort::createdAt),
    createdAtBeforeOrEqual = Date(),
).enqueue { result ->
    if (result.isSuccess) {
        val bannedUsers: List<BannedUser> = result.data()
    } else {
        // Handle result.error()
    }
}

Shadow Banning a User

Users can be shadow banned from an app entirely or from a channel. When a user is shadow banned, it will still be allowed to post messages, but any message sent during, will have shadowed: true field. However, this will be invisible to the author of the message.

It's up to the client-side implementation to hide or otherwise handle these messages appropriately.

// Shadow ban user for 60 minutes from a channel 
channelClient.shadowBanUser(targetId = "user-id", reason = "Bad words", timeout = 60).enqueue { result -> 
     if (result.isSuccess) { 
         // User was shadow banned 
     } else { 
         // Handle result.error() 
     } 
} 
 
channelClient.removeShadowBan("user-id").enqueue { result -> 
    if (result.isSuccess) { 
        // Shadow ban was removed 
    } else { 
        // Handle result.error() 
    } 
}

Administrators can view shadow banned user status in queryChannels(), queryMembers() and queryUsers().

User Presence

User presence allows you to show when a user was last active and if they are online right now. Whenever you read a user the data will look like this:

{ 
    id: 'unique_user_id', 
    online: true, 
    status: 'Eating a veggie burger...', 
    last_active: '2019-01-07T13:17:42.375Z' 
}

The online field indicates if the user is online. The status field stores text indicating the current user status.

Marking a User Invisible

To mark a user invisible simply set the invisible property to true. You can also set a custom status message at the same time:

val user = User( 
    id = "user-id", 
    invisible = true, 
) 
client.connectUser(user, "user-token").enqueue { result -> 
    if (result.isSuccess) { 
        val user: ConnectionData = result.data() 
    } else { 
        // Handle result.error() 
    } 
}

When invisible is set to true, the current user will appear as offline to other users.

NOTE: User's invisible status can only be set while calling connectUser method.

Listening to User Presence Changes

These 3 endpoints allow you to watch user presence:

// You need to be watching some channels/queries to be able to get presence events. 
// Here are three different ways of doing that: 
 
// 1. Watch a single channel with presence = true set 
val watchRequest = WatchChannelRequest().apply { 
    data["members"] = listOf("john", "jack") 
    presence = true 
} 
channelClient.watch(watchRequest).enqueue { result -> 
    if (result.isSuccess) { 
        val channel: Channel = result.data() 
    } else { 
        // Handle result.error() 
    } 
} 
 
// 2. Query some channels with presence = true set 
val channelsRequest = QueryChannelsRequest( 
    filter = Filters.and( 
        Filters.eq("type", "messaging"), 
        Filters.`in`("members", listOf("john", "jack")), 
    ), 
    offset = 0, 
    limit = 10, 
).apply { 
    presence = true 
} 
client.queryChannels(channelsRequest).enqueue { result -> 
    if (result.isSuccess) { 
        val channels: List<Channel> = result.data() 
    } else { 
        // Handle result.error() 
    } 
} 
 
// 3. Query some users with presence = true set 
val usersQuery = QueryUsersRequest( 
    filter = Filters.`in`("id", listOf("john", "jack")), 
    offset = 0, 
    limit = 2, 
    presence = true, 
) 
client.queryUsers(usersQuery).enqueue { result -> 
    if (result.isSuccess) { 
        val users: List<User> = result.data() 
    } else { 
        // Handle result.error() 
    } 
} 
 
// Finally, subscribe to presence to events 
client.subscribeFor<UserPresenceChangedEvent> { event -> 
    // Handle change 
}

Users' online status change can be handled via event delegation by subscribing to the user.presence.changed event the same you do for any other event.

Typing Indicators

You will need to take care of four things:

  1. Send an event typing.start when the user starts typing
  2. Send an event typing.stop after the user stopped typing
  3. Handle the two events and use them to toggle the typing indicator UI
  4. Use parent_id field of the event to indicate that typing is happening in a thread

Sending Typing Events

// Sends a typing.start event at most once every two seconds 
channelClient.keystroke().enqueue() 
 
// Sends a typing.start event for a particular thread 
channelClient.keystroke(parentId = "threadId").enqueue() 
 
// Sends the typing.stop event 
channelClient.stopTyping().enqueue()

When sending events on user input, you should make sure to follow some best-practices to avoid bugs.

  1. Only send typing.start when the user starts typing
  2. Send typing.stop after a few seconds since the last keystroke

Observing Typing Events

// Add typing start event handling 
channelClient.subscribeFor<TypingStartEvent> { typingStartEvent -> 
    // Handle event 
} 
 
// Add typing stop event handling 
channelClient.subscribeFor<TypingStopEvent> { typingStopEvent -> 
    // Handle event 
}

Because clients might fail at sending typing.stop event all Chat clients periodically prune the list of typing users.

Events

Listening for Channel Events

As soon as you call watch on a Channel or queryChannels you’ll start to listen to these events. You can hook into specific events:

val channelClient = client.channel("messaging", "channelId") 
 
// Subscribe for new message events 
val disposable: Disposable = channelClient.subscribeFor<NewMessageEvent> { newMessageEvent -> 
    val message = newMessageEvent.message 
} 
 
// Dispose when you want to stop receiving events 
disposable.dispose()

You can also listen to all events at once:

val disposable: Disposable = channelClient.subscribe { event: ChatEvent -> 
    when (event) { 
        // Check for specific event types 
        is NewMessageEvent -> { 
            val message = event.message 
        } 
    } 
} 
 
// Dispose when you want to stop receiving events 
disposable.dispose()

Listening for Client Events

Not all events are specific to channels. Events such as the user's status has changed, the users' unread count has changed, and other notifications are sent as client events. These events can be listened to through the client directly:

// Subscribe for User presence events 
client.subscribeFor<UserPresenceChangedEvent> { event -> 
    // Handle change 
} 
 
// Subscribe for just the first ConnectedEvent 
client.subscribeForSingle<ConnectedEvent> { event -> 
    // Use event data 
    val unreadCount = event.me.totalUnreadCount 
    val unreadChannels = event.me.unreadChannels 
}

Listening for Connection Events

The official SDKs make sure that a connection to Stream is kept alive at all times and that chat state is recovered when the user's internet connection comes back online. Your application can subscribe to changes to the connection using client events.

client.subscribeFor( 
    ConnectedEvent::class, 
    ConnectingEvent::class, 
    DisconnectedEvent::class, 
) { event -> 
    when (event) { 
        is ConnectedEvent -> { 
            // Socket is connected 
        } 
        is ConnectingEvent -> { 
            // Socket is connecting 
        } 
        is DisconnectedEvent -> { 
            // Socket is disconnected 
        } 
    } 
}

Stop Listening for Events

It is a good practice to unregister event handlers once they are not in use anymore. Doing so will save you from performance degradations coming from memory leaks or even from errors and exceptions (i.e. null pointer exceptions)

val disposable: Disposable = client.subscribe { /* ... */ } 
disposable.dispose()

Logging

Enabling logs

By default, logging is disabled. You can enable logs and set a log level when initializing ChatClient:

val client = ChatClient.Builder("apiKey", context)
    .logLevel(ChatLogLevel.ALL)
    .build()

Intercepting logs

If you need to intercept logs, you can also pass in your own ChatLoggerHandler:

val client = ChatClient.Builder("apiKey", context)
    .logLevel(ChatLogLevel.ALL)
    .loggerHandler(object : ChatLoggerHandler {
        override fun logD(tag: Any, message: String) {
            // custom logging
        }
        ...
     })
     .build()

Finding logs

All SDK log tags have the Chat: prefix, so you can filter for that those in the logs:

adb logcat com.your.package | grep "Chat:"

Here's a set of useful tags for debugging network communication:

  • Chat:Http
  • Chat:Events
  • Chat:SocketService
Clone this wiki locally