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 { /* ... */ }
}
}
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()
}
}
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()
}
}
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()
}
}
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:
- Using limit and offset
- 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 { /* ... */ }
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 { /* ... */ }
Name | Type | Description | Default | Optional |
---|---|---|---|---|
pinned | Boolean | Indicates whether the message is pinned or not | false | ✓ |
pinnedAt | Date | Date when the message got pinned | - | ✓ |
pinExpires | Date | Date when the message pin expires. An empty value means that message does not expire | null | ✓ |
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()
}
}