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()
}
}
Threads & 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. To create a thread you simply send a message with a parent_id
. Have a look at the example below:
const reply = await channel.sendMessage({
text: 'Hey, I am replying to a message!',
parent_id: parentID,
show_in_channel: false,
});
import StreamChat
/// 1: Create a `ChannelId` that represents the channel you want to get a message from.
let channelId = ChannelId(type: .messaging, id: "general")
/// 2: Create a `MessageId` that represents the message you want to get.
let messageId = "message-id"
/// 3: Use the `ChatClient` to create a `ChatMessageController` with the `ChannelId` and message id.
let messageController = chatClient.messageController(cid: channelId, messageId: messageId)
/// 4: Call `ChatMessageController.createNewReply` to start a thread.
messageController.createNewReply(text: "What's up?")
$response = $channel->sendMessage([
'text' => 'Hey, I am replying to a message!',
'parent_id' => 'parent-id',
'show_in_channel' => false
],
'jenny'
);
final reply = await channel.sendMessage(
Message(text: 'Hey, I am replying to a message!',
parentId: parentID,
showInChannel: false,
));
// Not yet supported in the Unreal SDK
channel.send_message(
{"text": "Hey, I am replying to a message!", "parent_id": msg["id"]},
user["id"],
)
channel.send_message(
{text: "Hey, I am replying to a message!", parent_id: msg["id"]},
user["id"],
)
reply := &Message{Text: "Hey, I am replying to a message!", ParentID: msg.ID}
channel.SendMessage(ctx, reply, user.ID)
await messageClient.SendMessageToThreadAsync(
channel.Type,
channel.Id,
new MessageRequest { Text = "Hey, I am replying to a message!" },
user.Id,
parentId: message.Id);
// Send simple message with text only
var parentMessage = await channel.SendNewMessageAsync("Let's start a thread!");
var messageInThread = await channel.SendNewMessageAsync(new StreamSendMessageRequest
{
ParentId = parentMessage.Id, // Write in thread
ShowInChannel = false, // Optionally send to both thread and the main channel like in Slack
Text = "Hello",
});
// Android SDK
Message message = new Message();
message.setText("Hello there!");
message.setParentId(parentMessage.getId());
// Send the message to the channel
channelClient.sendMessage(message).enqueue(result -> {
if (result.isSuccess()) {
Message sentMessage = result.data();
} else {
// Handle result.error()
}
});
// Backend SDK
Message.send(type, id)
.message(
MessageRequestObject.builder()
.text("Hey, I am replying to a message!")
.parentId(parentId)
.showInChannel(false)
.userId(userId)
.build())
.request();
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.
Thread Pagination
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 { /* ... */ }
// retrieve the latest 20 messages inside the thread
await channel.getReplies(parentMessageId, {limit: 20});
// retrieve the 20 more messages before the message with id "42"
await channel.getReplies(parentMessageId, {limit: 20, id_lte: "42"});
// retrive the oldest 20 messages in the thread
await channel.getReplies(parentMessageId, {limit: 20}, [{created_at: 1}]); // default sort is created_at: -1
let messageController = client.messageController(cid: .init(type: .messaging, id: "general"), messageId: "message-id")
messageController.synchronize() { error in
if error == nil {
messageController.replies
messageController.loadNextReplies()
}
}
import StreamChat
/// 1: Create a `ChannelId` that represents the channel you want to get a message from.
let channelId = ChannelId(type: .messaging, id: "general")
/// 2: Create a `MessageId` that represents the message you want to get.
let messageId = "message-id"
/// 3: Use the `ChatClient` to create a `ChatMessageController` with the `ChannelId` and message id.
let controller = chatClient.messageController(cid: channelId, messageId: messageId)
/// 4: Call `ChatMessageController.loadNextReplies` to get the replies.
controller.loadNextReplies(limit: 25) { error in
if let error = error {
// handle error
print(error)
} else {
// access messages
print(controller.replies)
controller.loadNextReplies(limit: 25) { error in
// handle error / access messages
print(error ?? controller.replies)
}
}
}
// Not yet supported in the Unreal SDK
# retrieve the first 20 messages inside the thread
channel.get_replies(msg["id"])
# retrieve the 20 more messages before the message with id "42"
channel.get_replies(msg["id"], limit=20, id_lte="42")
# retrieve the first 20 messages inside the thread
channel.get_replies(msg["id"])
# retrieve the 20 more messages before the message with id "42"
channel.get_replies(msg["id"], limit: 20, id_lte: "42")
// retrieve the first 20 messages inside the thread
channel.GetReplies(ctx, msg.ID, map[string][]string{
"limit": {"20"},
})
// retrieve the 20 more messages before the message with id "42"
channel.GetReplies(ctx, msg.ID, map[string][]string{
"limit": {"20"},
"id_lte": {"42"},
})
// retrieve the first 20 messages inside the thread
await messageClient.GetRepliesAsync(message.Id, new MessagePaginationParams
{
Limit = 20,
});
// retrieve the 20 more messages before the message with id "42"
await messageClient.GetRepliesAsync(message.Id, new MessagePaginationParams
{
Limit = 20,
IDLTE = "42",
});
// Will be implemented soon, raise a GitHub issue if you need this feature https://github.com/GetStream/stream-chat-unity/issues/
// Android SDK
int limit = 20;
// Retrieve the first 20 messages inside the thread
client.getReplies(parentMessage.getId(), limit).enqueue(result -> {
if (result.isSuccess()) {
List<Message> replies = result.data();
} else {
// Handle result.error()
}
});
// Retrieve the 20 more messages before the message with id "42"
client.getRepliesMore(parentMessage.getId(), "42", limit).enqueue(result -> { /* ... */ });
// Backend SDK
// retrieve the first 20 messages inside the thread
Message.getReplies(parentMessageId).limit(20).request();
// retrieve the 20 more messages before the message with id "42"
Message.getReplies(parentMessageId).limit(20).idLte("42").request();
Quote 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 { /* ... */ }
// Create the initial message
await channel.sendMessage({ id: 'first_message_id', text: 'The initial message' });
// Quote the initial message
const res = await channel.sendMessage({
id: 'message_with_quoted_message',
text: 'This is the first message that quotes another message',
quoted_message_id: 'first_message_id',
});
// Create the initial message
await channel.sendMessage(Message(id: 'first_message_id', text: 'The initial message' ));
// Quote the initial message
final res = await channel.sendMessage(Message(
id: 'message_with_quoted_message',
text: 'This is the first message that quotes another message',
quotedMessageId: 'first_message_id',
));
import StreamChat
/// 1: Create a `ChannelId` that represents the channel you want to send a message to.
let channelId = ChannelId(type: .messaging, id: "general")
/// 2: Use the `ChatClient` to create a `ChatChannelController` with the `ChannelId`.
let channelController = chatClient.channelController(for: channelId)
/// 3: Call `ChatChannelController.createNewMessage` with
/// `quotedMessageId` to create the message that quotes another message.
channelController.createNewMessage(text: "Hello!", quotedMessageId: "message-id") { result in
switch result {
case let .success(messageId):
print(messageId)
case let .failure(error):
print(error)
}
}
// Not yet supported in the Unreal SDK
channel.send_message(
{"text": "This is the first message that quotes another message", "quoted_message_id": msg["id"]},
user["id"],
)
channel.send_message(
{text: "This is the first message that quotes another message", quoted_message_id: msg["id"]},
user["id"],
)
await messageClient.SendMessageAsync(channel.Type, channel.Id, new MessageRequest
{
Text = "This is the first message that quotes another message",
QuotedMessageId = msg.Id, // Quote the initial message
}, user.Id);
msg := &Message{Text: "This is the first message that quotes another message"}
channel.SendMessage(ctx, msg, user.ID, func(msg *messageRequest) {
msg.Message.QuotedMessageId = originalMessage.ID // Quote the initial message
})
var messageWithQuote = await channel.SendNewMessageAsync(new StreamSendMessageRequest
{
QuotedMessage = quotedMessage,
Text = "Hello",
});
// Android SDK
Message message = new Message();
message.setText("This message quotes another message!");
message.setReplyMessageId(originalMessage.getId());
channelClient.sendMessage(message).enqueue(result -> { /* ... */ });
// Backend SDK
Message.send(type, id)
.message(
MessageRequestObject.builder()
.text("This is the first message that quotes another message")
.quotedMessageId("first_message_id")
.userId(userId)
.build())
.request();
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"
}
}
// Not yet supported in the Unity SDK
Quoted messages are only available one level deep. If you want to access deeply quoted messages you will have to fetch them directly either finding the message in response of channel.query()
or querying message by its ID. Example: Message A
quotes Message B
and Message B
quotes Message C
. In this case it’s not possible to access Message C
through Message A
directly. You have to fetch Message B
from API in order to access it
Thread List
Query Threads
It is possible to query the list of threads that current user is participant of. This is useful to create thread list similar to what Slack or Discord has. Response will include threads with unread replies first, sorted by the latest reply timestamp in descending order. Threads with no unread replies will follow, also sorted by the latest reply timestamp in descending order.
const threads = await client.queryThreads();
for (const thread of threads) {
const state = thread.state.getLatestValue();
console.log(state.participants);
console.log(state.replies);
console.log(state.parentMessage.text);
console.log(state.read); // read states of the participants
}
let query = ThreadListQuery(watch: true)
let threadListController = chatClient.threadListController(query: query)
threadListController.synchronize { error in
print(threadListController.threads)
}
{
"threads": [
{
"channel_cid": "messaging:E845FF77-2",
"channel": {
"id": "E845FF77-2",
"type": "messaging",
"cid": "messaging:E845FF77-2",
"created_by": {/*...*/},
"members": [/*...*/],
"member_count": 3,
"config": {/*...*/},
"name": "Group"
// ...
},
"parent_message_id": "7d362713-ce68-4c32-95fc-7e6800c2f167",
"parent_message": {
"id": "7d362713-ce68-4c32-95fc-7e6800c2f167",
"text": "Parent 2",
"type": "regular",
// ...
},
"created_by_user_id": "daniel",
"created_by": {/*...*/},
"reply_count": 2,
"participant_count": 2,
"thread_participants": [
{
"thread_id": "7d362713-ce68-4c32-95fc-7e6800c2f167",
"user_id": "luke_skywalker",
"user": {/*...*/},
// ...
},
],
"last_message_at": "2024-02-05T20:09:47.167368Z",
"created_at": "2024-02-05T20:09:45.379241Z",
"updated_at": "2024-02-05T20:09:47.167368Z",
"title": "Parent 2",
"latest_replies": [
{
"id": "bca4708a-87fc-4cee-9441-86eec59e9c12",
"text": "1",
"type": "reply",
"user": {/*...*/},
// ...
},
],
"read": [
{
"user": {
"id": "luke_skywalker",
"role": "user",
// ...
},
"last_read": "0001-01-01T00:00:00Z",
"unread_messages": 2
}
]
},
],
"duration": "108.20ms"
}
You can also paginate over the thread list or request specific number of latest replies per thread.
const { threads: page1, next: next1 } = await client.queryThreads({
reply_limit: 10,
participant_limit: 10,
member_limit: 0,
limit: 25,
watch: true,
});
// request the next page
const { threads: page2, next: next2 } = await client.queryThreads({
reply_limit: 10,
participant_limit: 10,
member_limit: 0,
limit: 25,
watch: true,
next: next1
})
let query = ThreadListQuery(
watch: true,
limit: 10,
replyLimit: 5,
participantLimit: 25,
next: nextCursor
)
let threadListController = chatClient.threadListController(query: query)
threadListController.synchronize { error in
print(threadListController.threads)
}
// also
threadListController.loadMoreThreads(limit: 10) { result in
// …
}
Following is the list of supported options for querying threads:
name | type | description | default | optional |
---|---|---|---|---|
reply_limit | number | Number of latest replies to fetch per thread | 2 | ✓ |
participant_limit | number | Number of thread participants to request per thread | 100 | ✓ |
limit | number | Maximum number of threads to return in response | 10 | ✓ |
watch | boolean | If true, all the channels corresponding to threads returned in response will be watched | true | ✓ |
member_limit | number | Number of members to request per thread channel | 100 | ✓ |
Thread List Management
ThreadManager
class offers built-in pagination handling for threads with own state store to access loaded threads. To access threads you can either subscribe to the ThreadManager
state or access the latest value by calling getLatestValue
on state property of the ThreadManager
.
const threadManager = new ThreadManager({ client });
const unsubscribe = threadManager.state.subscribe((nextState) => {
console.log(nextState.threads);
});
threadManager.loadNextPage();
// or
await threadManager.loadNextPage();
const { threads } = threadManager.state.getLatestValue();
Client instance already comes with one ThreadManager
instance which can be accessed via threads property of the client ( client.threads
).
Event Handling Of Threads
Each thread instance comes with registerSubscriptions
method which registers handlers to appropriate thread-related events. With subscriptions registered, the updates to the state are done automatically and can be accessed by subscribing to the state updates.
const { threads } = await client.queryThreads({ watch: true, limit: 1 });
const [thread] = threads;
thread.registerSubcsriptions();
const unsubscribe = thread.state.subscribe((nextState) => {
console.log(nextState.replies);
});
Note that watch
parameter is required when querying for threads in this case as without it, the channel to which the thread is linked won’t receive updates.
If you’re using ThreadManger
to manage your list of threads, you can call registerSubscriptions
method on the manager instance and the manager will automatically handle subscriptions (registration and cleanup) of the added and removed threads.
const threadManager = new ThreadManager({ client });
threadManager.registerSubscriptions();
await threadManager.loadNextPage();
// these threads are listening to linked channel events
const { threads } = threadManager.state.getLatestValue();
Get Thread By ID
This endpoint returns a thread by the given threadId.
const threadId = '<parent_message_id>';
const threadResponse = await client.getThread(threadId, {
watch: true, // optional
reply_limit: 3, // optional
member_limit: 0, // optional
participant_limit: 10, // optional
});
Update Thread Title and Custom Data
You can assign a title and any additional custom data to each thread.
const { thread: updatedThread } = client.partialUpdateThread(threadId, {
set: {
title: 'updated title',
description: 'updated description',
},
})
console.log(updatedThread.title)
console.log(updatedThread.description)
messageController.updateThread(
title: "New Title",
extraData: ["Hello": .string("World")]) { result in
// …
}
Similarly to remove any custom property from thread:
await client.partialUpdateThread(threadId, {
unset: ['title'],
})
messageController.updateThread(
title: nil,
unsetProperties: ["field-to-clear"]) { result in
// …
}
Threads Unread Count
Total number of unread threads is available in response of connectUser endpoint
const { me } = await clientUser1.connectUser({ id: user1 }, token);
console.log(me.unread_threads)
let connectedUser = try await chatClient.connectUser(
userInfo: .init(id: "john-doe"),
token: "<my token>"
)
print(connectedUser.state.user.unreadCount.threads)
If you would like to mark specific thread as read or unread, you can do that as following
await channel.markRead({
thread_id: 'parent_message_id',
});
await channel.markUnread({
thread_id: 'parent_message_id',
});
let channelId = ChannelId(type: .messaging, id: "general")
let messageController = chatClient.messageController(
cid: channelId,
messageId: "thread-id"
)
messageController.markThreadRead { error in
// …
}
messageController.markThreadUnread { error in
// …
}
To get unread count per thread for a current user, you can use the getUnreadCount
endpoint as following
const res = await client.getUnreadCount();
console.log(res.total_unread_threads_count);
console.log(res.threads[0].last_read)
console.log(res.threads[0].last_read_message_id)
console.log(res.threads[0].parent_message_id)
console.log(res.threads[0].unread_count)