Skip to main content
Version: v5

Messages

Message represents single communication inside a channel. Besides typical communication-related fields like text, created date, or message sender, it also contains other useful components like:

  • Attachments - list of files inside the message
  • Reactions - users' reactions for the message. Common examples are: "like", "love", etc.

Sending Messages

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

Uploading Files

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 100 MB for the Stream Chat CDN.

Uploading an Image

You can upload an image by calling sendImage and including a File:

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 { /* ... */ }
}
}
note

Attachments need to be linked to the message after the upload is completed.

Uploading a File

You can upload an image by calling sendFile and including 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

Using Your Own CDN

The SDK allows you to use your own CDN by creating your own implementation of the FileUploader interface, and pass it to ChatClient.Builder.

The example below show how to change where files are uploaded:

// Create a custom FileUploader
class MyFileUploader : FileUploader {
override fun sendFile(
channelType: String,
channelId: String,
userId: String,
connectionId: String,
file: File,
): Result<String> {
return try {
// Send the file to your own CDN
val url = ...
// Return a Result object with file url
Result.success(url)
} catch (e: Exception) {
// Return a Result object with exception in case upload failed
Result.error(e)
}
}
...
}

// 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. Reactions can be customized so that you are able to use any type of reaction your application requires.

Similar to other objects in Stream Chat, reactions allow you to add custom data to the reaction of your choice. This is helpful if you want to customize the reaction logic.

Sending a Reaction

You can send a reaction by calling sendReaction:

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

val reaction = Reaction(
messageId = "message-id",
type = "like",
score = 1,
extraData = mutableMapOf("customField" to 1)
)
channelClient.sendReaction(reaction).enqueue { result ->
if (result.isSuccess) {
val sentReaction: Reaction = result.data()
} else {
// Handle result.error()
}
}
note

Custom data for reactions is limited to 1KB.

Replacing a Reaction

You can enforce users to only have one reaction to the particular message. To do so, call sendReaction with enforceUnique parameter set to true:

// 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

You can delete a reaction by calling deleteReaction with messageId and reactionType:

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 10 most recent reactions. You can also retrieve more reactions and paginate using the following logic:

// Get the second 10 reactions 
channelClient.getReactions(
messageId = "message-id",
offset = 10,
limit = 10,
).enqueue { /* ... */ }

You can also get the reactions added after a particular reaction:

// Get 10 reactions after particular reaction
channelClient.getReactions(
messageId = "message-id",
firstReactionId = "reaction-id",
limit = 10,
).enqueue { /* ... */ }

Cumulative (Clap) Reactions

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.

To do this, you only need to include a score for the reaction (for example user X clapped 25 times) and the API will return the sum of all reaction scores as well as each user individual scores (for example clapped 475 times, user Y clapped 14 times).

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

Threads and Replies

Threads and replies provide your users with a way to go into more detail about a specific topic. This 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 parentId. 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()
}
}
note

If you specify showInChannel, 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 mentions as any other message.

Retrieving Thread Messages

Thread messages are separated from regular messages and won't be included in regular message response. In order to get messages from a particular thread - you need to pass parent message id in messages requests.

// 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, you can 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 replyMessageId when sending a message:

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

Based on the provided replyMessageId, the Message::replyTo field is automatically enriched when querying channels with messages.

Searching Messages

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(
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().messages
} else {
// Handle result.error()
}
}
note

After sending a message, it may take several minutes for the message to be returned in search results.

There are two ways to paginate through search results:

  1. Using limit and offset
  2. Using limit and next/previous values

Limit and offset will allow you to access up to 1000 results matching your query. You will not be able to sort using limit and offset. The results will instead be sorted by relevance and message ID.

Next pagination will allow you to access all search results that match your query, and you will be able to sort using any filter-able fields and custom fields.

Pages of sort results will be returned with next and previous strings, which tell the API where to start searching from and what sort order to use. You can supply those values as a next parameter when making a query to get a new page of results.

val channelFilter = Filters.`in`("members", listOf("john"))
val messageFilter = Filters.autocomplete("text", "supercalifragilisticexpialidocious")

// First 10 results sorted by relevance in descending order
val page1: SearchMessagesResult = client.searchMessages(
channelFilter = channelFilter,
messageFilter = messageFilter,
limit = 10,
sort = QuerySort.desc("relevance")
).execute().data()

// Next 10 results with the same sort order embedded in the next value
val page2: SearchMessagesResult = client.searchMessages(
channelFilter = channelFilter,
messageFilter = messageFilter,
limit = 10,
next = page1.previous
).execute().data()

// The previous 10 results
val page1Again: SearchMessagesResult = client.searchMessages(
channelFilter = channelFilter,
messageFilter = messageFilter,
limit = 10,
next = page2.previous
).execute().data()

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

Silent Messages

Silent messages are special messages that don't increase the unread messages count nor mark a channel as unread. They can be used to send system or transactional messages to channels such as:

  • "your ride is waiting for you"
  • "James updated the information for the trip"
  • "You and Jane are now matched"
  • Etc…

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 { /* ... */ }
note

Existing messages cannot be turned into a silent message or vice versa.

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 ChannelClient::pinMessage and ChannelClient::unpinMessage methods.

A new message can be pinned when it is sent by setting the pinned and pinExpires properties.

// 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 { /* ... */ }
NameTypeDescriptionDefaultOptional
pinnedBooleanIndicates whether the message is pinned or notfalse
pinnedAtDateDate when the message got pinned-
pinExpiresDateDate when the message pin expires. An empty value means that message does not expirenull

Retrieving Pinned Messages

You can easily retrieve the last 10 pinned messages of a Channel using the pinnedMessages property:

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

Learn more about querying channels on the Channels page.

Searching for Pinned Messages

You can also use a search filter 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()
}
}

Did you find this page helpful?