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.
var resp = await chat.SendMessageAsync("messaging", channelId,
new SendMessageRequest
{
Message = new MessageRequest
{
Text = "This is a reply in a thread",
UserID = userId,
ParentID = parentMessageId
}
});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 first 20 replies
var resp = await chat.GetRepliesAsync(parentMessageId, new { limit = 20 });
// Get older replies
var olderResp = await chat.GetRepliesAsync(parentMessageId, new { limit = 20, id_lte = "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.
var resp = await chat.SendMessageAsync("messaging", channelId,
new SendMessageRequest
{
Message = new MessageRequest
{
Text = "I agree with this point",
UserID = userId,
QuotedMessageID = originalMessageId
}
});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.
var response = await Client.QueryThreadsAsync(new StreamQueryThreadsRequest
{
Watch = true,
Limit = 10,
});
foreach (var thread in response.Threads)
{
// Threads are cached, watched and kept in sync with realtime events
Debug.Log(thread.ParentMessage.Text);
Debug.Log(thread.LatestReplies);
Debug.Log(thread.ThreadParticipants);
Debug.Log(thread.Read);
}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.
var resp = await chat.QueryThreadsAsync(new QueryThreadsRequest
{
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" } } },
},
Sort = new List<SortParamRequest>
{
new SortParamRequest { Field = "created_at", Direction = -1 }
},
Limit = 10,
UserID = userId
});
// Get next page
if (resp.Data.Next != null)
{
var nextPage = await chat.QueryThreadsAsync(new QueryThreadsRequest
{
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" } } },
},
Sort = new List<SortParamRequest>
{
new SortParamRequest { Field = "created_at", Direction = -1 }
},
Limit = 10,
UserID = userId,
Next = resp.Data.Next
});
}Getting a Thread by ID
Retrieve a specific thread using the parent message ID.
// The returned IStreamThread is auto-watched (watch defaults to true) and stays in
// sync with realtime events via Updated / ReplyReceived / ReadStateChanged.
var thread = await Client.GetThreadAsync("parent-message-id",
replyLimit: 10, participantLimit: 25);Updating Thread Title and Custom Data
Assign a title and custom data to a thread.
var thread = await Client.GetThreadAsync("parent-message-id");
// Set title and custom fields; unset a previously set field
await thread.UpdatePartialAsync(
setFields: new Dictionary<string, object>
{
{ "title", "Project Discussion" },
{ "priority", "high" },
},
unsetFields: new[] { "priority" });Thread Unread Counts
Total Unread Threads
The total number of unread threads is available after connecting.
// Available immediately after connect on IStreamLocalUserData
var unreadThreads = Client.LocalUserData.UnreadThreads;
Debug.Log(unreadThreads);
// The same total is also available on demand from the server
var unreadCounts = await Client.GetLatestUnreadCountsAsync();
Debug.Log(unreadCounts.TotalUnreadThreadsCount);Marking Threads as Read or Unread
var thread = await Client.GetThreadAsync("parent-message-id");
// Mark this thread as read for the local user
await thread.MarkReadAsync();
// Mark this thread as unread starting from the parent message
await thread.MarkUnreadAsync();
// Equivalent helpers from the parent message of the thread
IStreamMessage parentMessage = thread.ParentMessage;
await parentMessage.MarkThreadAsReadAsync();
await parentMessage.MarkThreadAsUnreadAsync();
// Or by parent message id when you already have the channel
await thread.Channel.MarkThreadAsReadAsync(thread.ParentMessageId);
await thread.Channel.MarkThreadAsUnreadAsync(thread.ParentMessageId);Unread Count Per Thread
var unreadCounts = await Client.GetLatestUnreadCountsAsync();
Debug.Log(unreadCounts.TotalUnreadThreadsCount);
foreach (var thread in unreadCounts.UnreadThreads)
{
Debug.Log(thread.ParentMessageId);
Debug.Log(thread.UnreadCount);
Debug.Log(thread.LastRead);
Debug.Log(thread.LastReadMessageId);
}Thread Manager
The ThreadManager class provides built-in pagination and state management for threads.
// Get notified when a thread starts or stops being tracked locally
Client.ThreadTracked += thread => { /* a new IStreamThread is now tracked */ };
Client.ThreadUntracked += thread => { /* an IStreamThread is no longer tracked */ };
// Load threads. Watch defaults to true so realtime updates are delivered.
var response = await Client.QueryThreadsAsync(new StreamQueryThreadsRequest
{
Watch = true,
Limit = 10,
});
// Each returned IStreamThread is stateful: it is cached and kept in sync with
// realtime events automatically. Subscribe to per-thread events to react to
// changes (e.g. new replies, title / custom data updates, read state changes).
foreach (var thread in response.Threads)
{
thread.Updated += changedThread => { /* title or custom data changed */ };
thread.ReplyReceived += (changedThread, reply) => { /* new reply arrived */ };
thread.ReadStateChanged += changedThread => { /* unread state changed */ };
}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();