const reply = await channel.sendMessage({
text: "This is a reply in a thread",
parent_id: parentMessageId,
show_in_channel: false,
});Threads & Replies
Threads allow users to reply to specific messages without cluttering the main channel conversation. A thread is created when a message is sent with a parent_id referencing another message.
Starting a Thread
Send a message with a parent_id to start a thread or add a reply to an existing thread.
val message = Message(
text = "This is a reply in a thread",
parentId = parentMessage.id,
)
channelClient.sendMessage(message).enqueue { result ->
if (result is Result.Success) {
val sentMessage = result.value
} else {
// Handle Result.Failure
}
}let channelId = ChannelId(type: .messaging, id: "general")
let messageId = "parent-message-id"
let messageController = chatClient.messageController(cid: channelId, messageId: messageId)
messageController.createNewReply(text: "This is a reply in a thread")final reply = await channel.sendMessage(
Message(
text: "This is a reply in a thread",
parentId: parentMessageId,
showInChannel: false,
),
);// Android SDK
Message message = new Message();
message.setText("This is a reply in a thread");
message.setParentId(parentMessage.getId());
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("This is a reply in a thread")
.parentId(parentId)
.showInChannel(false)
.userId(userId)
.build())
.request();channel.send_message(
{"text": "This is a reply in a thread", "parent_id": parent_message_id},
user_id,
)$response = $channel->sendMessage([
"text" => "This is a reply in a thread",
"parent_id" => $parentMessageId,
"show_in_channel" => false,
], $userId);channel.send_message(
{ text: "This is a reply in a thread", parent_id: parent_message_id },
user_id,
)reply := &Message{Text: "This is a reply in a thread", ParentID: parentMessage.ID}
channel.SendMessage(ctx, reply, userID)await messageClient.SendMessageToThreadAsync(
channel.Type,
channel.Id,
new MessageRequest { Text = "This is a reply in a thread" },
userId,
parentId: parentMessage.Id);var parentMessage = await channel.SendNewMessageAsync("Starting a thread");
var reply = await channel.SendNewMessageAsync(new StreamSendMessageRequest
{
ParentId = parentMessage.Id,
ShowInChannel = false,
Text = "This is a reply in a thread",
});Thread Parameters
| Name | Type | Description | Default | Optional |
|---|---|---|---|---|
| parent_id | string | ID of the parent message to reply to | ||
| show_in_channel | boolean | If true, the reply appears both in the thread and the main channel | false | ✓ |
Messages in threads support the same features as regular messages: reactions, attachments, and mentions.
Paginating Thread Replies
When querying a channel, thread replies are not included by default. The parent message includes a reply_count field. Use getReplies to fetch thread messages.
// Get the latest 20 replies
const replies = await channel.getReplies(parentMessageId, { limit: 20 });
// Get older replies (before message with ID "42")
const olderReplies = await channel.getReplies(parentMessageId, {
limit: 20,
id_lte: "42",
});
// Get oldest replies first
const oldestFirst = await channel.getReplies(parentMessageId, { limit: 20 }, [
{ created_at: 1 },
]);// Get the first 20 replies
client.getReplies(parentMessage.id, limit = 20).enqueue { result ->
if (result is Result.Success) {
val replies: List<Message> = result.value
} else {
// Handle Result.Failure
}
}
// Get 20 more replies before message with ID "42"
client.getRepliesMore(
messageId = parentMessage.id,
firstId = "42",
limit = 20,
).enqueue { /* ... */ }let messageController = client.messageController(
cid: .init(type: .messaging, id: "general"),
messageId: "parent-message-id"
)
messageController.synchronize { error in
if error == nil {
print(messageController.replies)
messageController.loadNextReplies()
}
}// Get the first 20 replies
final replies = await channel.getReplies(parentMessageId, limit: 20);
// Get older replies
final olderReplies = await channel.getReplies(
parentMessageId,
limit: 20,
options: {"id_lte": "42"},
);// Android SDK
int limit = 20;
// Get the first 20 replies
client.getReplies(parentMessage.getId(), limit).enqueue(result -> {
if (result.isSuccess()) {
List<Message> replies = result.data();
} else {
// Handle result.error()
}
});
// Get 20 more replies before message with ID "42"
client.getRepliesMore(parentMessage.getId(), "42", limit).enqueue(result -> { /* ... */ });
// Backend SDK
Message.getReplies(parentMessageId).limit(20).request();
Message.getReplies(parentMessageId).limit(20).idLte("42").request();# Get the first 20 replies
channel.get_replies(parent_message_id, limit=20)
# Get older replies
channel.get_replies(parent_message_id, limit=20, id_lte="42")# Get the first 20 replies
channel.get_replies(parent_message_id, limit: 20)
# Get older replies
channel.get_replies(parent_message_id, limit: 20, id_lte: "42")// Get the first 20 replies
channel.GetReplies(ctx, parentMessageID, map[string][]string{
"limit": {"20"},
})
// Get older replies
channel.GetReplies(ctx, parentMessageID, map[string][]string{
"limit": {"20"},
"id_lte": {"42"},
})// Get the first 20 replies
await messageClient.GetRepliesAsync(message.Id, new MessagePaginationParams
{
Limit = 20,
});
// Get older replies
await messageClient.GetRepliesAsync(message.Id, new MessagePaginationParams
{
Limit = 20,
IDLTE = "42",
});Inline Replies
Reply to a message inline without creating a thread. The referenced message appears within the new message. Use quoted_message_id instead of parent_id.
const message = await channel.sendMessage({
text: "I agree with this point",
quoted_message_id: originalMessageId,
});val message = Message(
text = "I agree with this point",
replyMessageId = originalMessage.id,
)
channelClient.sendMessage(message).enqueue { /* ... */ }let channelId = ChannelId(type: .messaging, id: "general")
let channelController = chatClient.channelController(for: channelId)
channelController.createNewMessage(
text: "I agree with this point",
quotedMessageId: "original-message-id"
) { result in
switch result {
case let .success(messageId):
print(messageId)
case let .failure(error):
print(error)
}
}final message = await channel.sendMessage(Message(
text: "I agree with this point",
quotedMessageId: originalMessageId,
));// Android SDK
Message message = new Message();
message.setText("I agree with this point");
message.setReplyMessageId(originalMessage.getId());
channelClient.sendMessage(message).enqueue(result -> { /* ... */ });
// Backend SDK
Message.send(type, id)
.message(
MessageRequestObject.builder()
.text("I agree with this point")
.quotedMessageId(originalMessageId)
.userId(userId)
.build())
.request();channel.send_message(
{"text": "I agree with this point", "quoted_message_id": original_message_id},
user_id,
)channel.send_message(
{ text: "I agree with this point", quoted_message_id: original_message_id },
user_id,
)msg := &Message{Text: "I agree with this point"}
channel.SendMessage(ctx, msg, userID, func(msg *messageRequest) {
msg.Message.QuotedMessageId = originalMessage.ID
})await messageClient.SendMessageAsync(channel.Type, channel.Id, new MessageRequest
{
Text = "I agree with this point",
QuotedMessageId = originalMessage.Id,
}, userId);var message = await channel.SendNewMessageAsync(new StreamSendMessageRequest
{
QuotedMessage = originalMessage,
Text = "I agree with this point",
});When querying messages, the quoted_message field is automatically populated:
{
"id": "new-message-id",
"text": "I agree with this point",
"quoted_message_id": "original-message-id",
"quoted_message": {
"id": "original-message-id",
"text": "The original message text"
}
}Inline replies are only available one level deep. If Message A replies to Message B, and Message B replies to Message C, you cannot access Message C through Message A. Fetch Message B directly to access its referenced message.
Thread List
Query all threads that the current user participates in. This is useful for building thread list views similar to Slack or Discord.
Querying Threads
Threads are returned with unread replies first, 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.parentMessage.text);
console.log(state.replies);
console.log(state.participants);
console.log(state.read);
}val request = QueryThreadsRequest()
client.queryThreadsResult(request).enqueue { result ->
if (result is Result.Success) {
val threads: List<Thread> = result.value.threads
} else {
// Handle Result.Failure
}
}let query = ThreadListQuery(watch: true)
let threadListController = chatClient.threadListController(query: query)
threadListController.synchronize { error in
print(threadListController.threads)
}{
"threads": [
{
"channel_cid": "messaging:general",
"channel": {
"id": "general",
"type": "messaging",
"name": "General"
},
"parent_message_id": "parent-123",
"parent_message": {
"id": "parent-123",
"text": "Original message",
"type": "regular"
},
"created_by_user_id": "user-1",
"reply_count": 5,
"participant_count": 3,
"thread_participants": [
{
"user_id": "user-1",
"user": { "id": "user-1", "name": "Alice" }
},
{
"user_id": "user-2",
"user": { "id": "user-2", "name": "Bob" }
}
],
"last_message_at": "2024-12-11T15:30:00Z",
"latest_replies": [
{
"id": "reply-1",
"text": "Latest reply",
"type": "reply"
}
],
"read": [
{
"user": { "id": "user-1" },
"last_read": "2024-12-11T15:00:00Z",
"unread_messages": 2
}
]
}
]
}Query Options
| 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 fetch per thread | 100 | ✓ |
| limit | number | Maximum number of threads to return | 10 | ✓ |
| watch | boolean | If true, watch channels for the returned threads | true | ✓ |
| member_limit | number | Number of members to fetch per thread channel | 100 | ✓ |
Filtering and Sorting
Filter and sort threads using MongoDB-style query operators.
Supported Filter Fields
| Field | Type | Operators | Description |
|---|---|---|---|
channel_cid | string or list of strings | $eq, $in | Channel CID |
channel.disabled | boolean | $eq | Channel disabled status |
channel.team | string or list of strings | $eq, $in | Channel team |
parent_message_id | string or list of strings | $eq, $in | Parent message ID |
created_by_user_id | string or list of strings | $eq, $in | Thread creator’s user ID |
created_at | string (RFC3339) | $eq, $gt, $lt, $gte, $lte | Thread creation timestamp |
updated_at | string (RFC3339) | $eq, $gt, $lt, $gte, $lte | Thread update timestamp |
last_message_at | string (RFC3339) | $eq, $gt, $lt, $gte, $lte | Last message timestamp |
Supported Sort Fields
active_participant_countcreated_atlast_message_atparent_message_idparticipant_countreply_countupdated_at
Use 1 for ascending order and -1 for descending order.
// Get threads created by a specific user, sorted by creation date
const { threads, next } = await client.queryThreads({
filter: {
created_by_user_id: { $eq: "user-1" },
updated_at: { $gte: "2024-01-01T00:00:00Z" },
},
sort: [{ created_at: -1 }],
limit: 10,
});
// Get next page
const { threads: page2 } = await client.queryThreads({
filter: {
created_by_user_id: { $eq: "user-1" },
updated_at: { $gte: "2024-01-01T00:00:00Z" },
},
sort: [{ created_at: -1 }],
limit: 10,
next,
});val request = QueryThreadsRequest(
filter = Filters.and(
Filters.eq("created_by_user_id", userId),
Filters.greaterThanEquals("updated_at", date),
),
sort = QuerySortByField.descByName("created_at"),
limit = 10,
)
val result = client.queryThreadsResult(request).await().getOrThrow()
val threads = result.threads
val nextCursor = result.next
// Get next page
val nextRequest = request.copy(next = nextCursor)
val nextResult = client.queryThreadsResult(nextRequest).await().getOrThrow()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)
}
// Load more
threadListController.loadMoreThreads(limit: 10) { result in
// Handle result
}filter_conditions = {
"created_by_user_id": {"$eq": "user-1"},
"updated_at": {"$gte": "2024-01-01T00:00:00Z"},
}
sort_conditions = [{"created_at": -1}]
response = client.query_threads(
filter=filter_conditions,
sort=sort_conditions,
limit=10,
user_id=user_id,
)
# Get next page
if response.get("next"):
next_page = client.query_threads(
filter=filter_conditions,
sort=sort_conditions,
limit=10,
user_id=user_id,
next=response["next"],
)$filter = [
"created_by_user_id" => ["$eq" => "user-1"],
"updated_at" => ["$gte" => "2024-01-01T00:00:00Z"],
];
$sort = [["created_at" => -1]];
$response = $client->queryThreads($filter, $sort, [
"user_id" => $userId,
"limit" => 10,
]);
// Get next page
if (isset($response["next"])) {
$nextPage = $client->queryThreads($filter, $sort, [
"user_id" => $userId,
"limit" => 10,
"next" => $response["next"],
]);
}filter = {
"created_by_user_id" => { "$eq" => "user-1" },
"updated_at" => { "$gte" => "2024-01-01T00:00:00Z" },
}
sort = [{ "created_at" => -1 }]
response = client.query_threads(
filter: filter,
sort: sort,
limit: 10,
user_id: user_id,
)
# Get next page
if response["next"]
next_page = client.query_threads(
filter: filter,
sort: sort,
limit: 10,
user_id: user_id,
next: response["next"],
)
endlimit := 10
query := &QueryThreadsRequest{
Filter: map[string]any{
"created_by_user_id": map[string]any{"$eq": "user-1"},
"updated_at": map[string]any{"$gte": "2024-01-01T00:00:00Z"},
},
Sort: &SortParamRequestList{
{Field: "created_at", Direction: -1},
},
PagerRequest: PagerRequest{Limit: &limit},
UserID: userID,
}
resp, err := client.QueryThreads(ctx, query)
// Get next page
if resp.Next != nil {
query.Next = resp.Next
nextPage, err := client.QueryThreads(ctx, query)
}var filter = new Dictionary<string, object>
{
{ "created_by_user_id", new Dictionary<string, object> { { "$eq", "user-1" } } },
{ "updated_at", new Dictionary<string, object> { { "$gte", "2024-01-01T00:00:00Z" } } },
};
var sort = new List<SortParameter>
{
new SortParameter { Field = "created_at", Direction = SortDirection.Descending },
};
var opts = QueryThreadsOptions.Default
.WithFilter(filter)
.WithSortBy(sort)
.WithLimit(10)
.WithUserId(userId);
var response = await threadClient.QueryThreads(opts);
// Get next page
if (response.Next != null)
{
var nextPage = await threadClient.QueryThreads(opts.WithNext(response.Next));
}FilterCondition filter = FilterCondition.and(
FilterCondition.eq("created_by_user_id", "user-1"),
FilterCondition.gte("updated_at", "2024-01-01T00:00:00Z")
);
List<Sort> sort = Arrays.asList(
Sort.builder().field("created_at").direction(Sort.Direction.DESC).build()
);
Thread.QueryThreadsResponse response = Thread.queryThreads()
.userId(userId)
.filter(filter)
.sort(sort)
.limit(10)
.request();
// Get next page
if (response.getNext() != null) {
Thread.QueryThreadsResponse nextPage = Thread.queryThreads()
.userId(userId)
.filter(filter)
.sort(sort)
.limit(10)
.next(response.getNext())
.request();
}Getting a Thread by ID
Retrieve a specific thread using the parent message ID.
const thread = await client.getThread(parentMessageId, {
watch: true,
reply_limit: 10,
participant_limit: 25,
});val options = GetThreadOptions(
watch = true,
replyLimit = 10,
participantLimit = 25,
)
client.getThread(parentMessageId, options).enqueue { /* ... */ }Updating Thread Title and Custom Data
Assign a title and custom data to a thread.
// Set properties
const { thread } = await client.partialUpdateThread(threadId, {
set: {
title: "Project Discussion",
priority: "high",
},
});
// Remove properties
await client.partialUpdateThread(threadId, {
unset: ["priority"],
});client.partialUpdateThread(
messageId = threadId,
set = mapOf(
"title" to "Project Discussion",
"priority" to "high",
),
).enqueue { result ->
if (result is Result.Success) {
val title = result.value.title
val extraData = result.value.extraData
}
}// Set properties
messageController.updateThread(
title: "Project Discussion",
extraData: ["priority": .string("high")]
) { result in
// Handle result
}
// Remove properties
messageController.updateThread(
title: nil,
unsetProperties: ["priority"]
) { result in
// Handle result
}Thread Unread Counts
Total Unread Threads
The total number of unread threads is available after connecting.
const { me } = await client.connectUser({ id: "user-id" }, token);
console.log(me.unread_threads);
// Or access via thread manager
client.threads.registerSubscriptions();
const { unreadThreadCount } = client.threads.state.getLatestValue();let connectedUser = try await chatClient.connectUser(
userInfo: .init(id: "user-id"),
token: token
)
print(connectedUser.state.user.unreadCount.threads)Marking Threads as Read or Unread
// Mark thread as read
await channel.markRead({ thread_id: parentMessageId });
// Mark thread as unread
await channel.markUnread({ thread_id: parentMessageId });let channelId = ChannelId(type: .messaging, id: "general")
let messageController = chatClient.messageController(cid: channelId, messageId: threadId)
// Mark thread as read
messageController.markThreadRead { error in
// Handle error
}
// Mark thread as unread
messageController.markThreadUnread { error in
// Handle error
}Unread Count Per Thread
const response = await client.getUnreadCount();
console.log(response.total_unread_threads_count);
for (const thread of response.threads) {
console.log(thread.parent_message_id);
console.log(thread.unread_count);
console.log(thread.last_read);
}Thread Manager
The ThreadManager class provides built-in pagination and state management for threads.
// Access the client's thread manager
const threadManager = client.threads;
// Subscribe to state updates
const unsubscribe = threadManager.state.subscribe((state) => {
console.log(state.threads);
console.log(state.unreadThreadCount);
});
// Load threads
await threadManager.loadNextPage();
// Access current state
const { threads } = threadManager.state.getLatestValue();Event Handling
Register subscriptions to receive real-time updates for threads.
const { threads } = await client.queryThreads({ watch: true, limit: 10 });
const [thread] = threads;
// Register event handlers for a single thread
thread.registerSubscriptions();
const unsubscribe = thread.state.subscribe((state) => {
console.log(state.replies);
});The watch parameter is required when querying threads to receive real-time updates.
For ThreadManager, call registerSubscriptions once to automatically manage subscriptions for all loaded threads:
const threadManager = client.threads;
threadManager.registerSubscriptions();
await threadManager.loadNextPage();
// All threads are now listening to channel events
const { threads } = threadManager.state.getLatestValue();