# 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.

<codetabs>

<codetabs-item value="javascript" label="JavaScript">

```js
const reply = await channel.sendMessage({
  text: "This is a reply in a thread",
  parent_id: parentMessageId,
  show_in_channel: false,
});
```

</codetabs-item>

<codetabs-item value="kotlin" label="Kotlin">

```kotlin
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
  }
}
```

</codetabs-item>

<codetabs-item value="swift" label="Swift">

```swift
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")
```

</codetabs-item>

<codetabs-item value="dart" label="Dart">

```dart
final reply = await channel.sendMessage(
  Message(
    text: "This is a reply in a thread",
    parentId: parentMessageId,
  showInChannel: false,
  ),
);
```

</codetabs-item>

<codetabs-item value="java" label="Java">

```java
// 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
chat.sendMessage(type, id, SendMessageRequest.builder()
    .message(MessageRequest.builder()
        .text("This is a reply in a thread")
        .parentID(parentId)
        .showInChannel(false)
        .userID(userId)
        .build())
    .build()).execute();
```

</codetabs-item>

<codetabs-item value="python" label="Python">

```python
from getstream.models import MessageRequest

channel.send_message(
    MessageRequest(
        text="This is a reply in a thread",
        parent_id=parent_message_id,
        user_id=user_id,
    )
)
```

</codetabs-item>

<codetabs-item value="php" label="PHP">

```php
$response = $client->sendMessage("messaging", "general", new Models\SendMessageRequest(
    message: new Models\MessageRequest(
        text: "This is a reply in a thread",
        parentID: $parentMessageId,
        showInChannel: false,
        userID: $userId,
    ),
));
```

</codetabs-item>

<codetabs-item value="ruby" label="Ruby">

```ruby
require 'getstream_ruby'
Models = GetStream::Generated::Models

client.chat.send_message('messaging', channel_id, Models::SendMessageRequest.new(
  message: Models::MessageRequest.new(
    text: 'This is a reply in a thread',
    parent_id: parent_message_id,
    user_id: user_id
  )
))
```

</codetabs-item>

<codetabs-item value="go" label="Go">

```go
resp, err := channel.SendMessage(ctx, &getstream.SendMessageRequest{
    Message: getstream.MessageRequest{
        Text:     getstream.PtrTo("This is a reply in a thread"),
        ParentID: getstream.PtrTo(parentMessageID),
        UserID:   getstream.PtrTo(userID),
    },
})
```

</codetabs-item>

<codetabs-item value="csharp" label="C#">

```csharp
var resp = await chat.SendMessageAsync("messaging", channelId,
    new SendMessageRequest
    {
        Message = new MessageRequest
        {
            Text = "This is a reply in a thread",
            UserID = userId,
            ParentID = parentMessageId
        }
    });
```

</codetabs-item>

<codetabs-item value="unity" label="Unity">

```csharp
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",
});
```

</codetabs-item>

</codetabs>

### 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   | ✓        |

<admonition type="info">

Messages in threads support the same features as regular messages: reactions, attachments, and mentions.

</admonition>

## 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.

<codetabs>

<codetabs-item value="javascript" label="JavaScript">

```js
// 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 },
]);
```

</codetabs-item>

<codetabs-item value="kotlin" label="Kotlin">

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

</codetabs-item>

<codetabs-item value="swift" label="Swift">

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

</codetabs-item>

<codetabs-item value="dart" label="Dart">

```dart
// 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"},
);
```

</codetabs-item>

<codetabs-item value="java" label="Java">

```java
// 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
chat.getReplies(parentMessageId, GetRepliesRequest.builder().Limit(20).build()).execute();
chat.getReplies(parentMessageId, GetRepliesRequest.builder().Limit(20).IDLte("42").build()).execute();
```

</codetabs-item>

<codetabs-item value="python" label="Python">

```python
# Get the first 20 replies
client.chat.get_replies(parent_id=parent_message_id, limit=20)

# Get older replies
client.chat.get_replies(parent_id=parent_message_id, limit=20, id_lte="42")
```

</codetabs-item>

<codetabs-item value="ruby" label="Ruby">

```ruby
require 'getstream_ruby'
Models = GetStream::Generated::Models

# Get the first 20 replies
client.chat.get_replies(parent_message_id, 20)

# Get older replies
client.chat.get_replies(parent_message_id, 20, nil, nil, nil, '42')
```

</codetabs-item>

<codetabs-item value="go" label="Go">

```go
limit := 20

// Get the first 20 replies
resp, err := client.Chat().GetReplies(ctx, parentMessageID, &getstream.GetRepliesRequest{
    Limit: getstream.PtrTo(limit),
    Sort:  []getstream.SortParamRequest{},
})

// Get older replies
resp, err = client.Chat().GetReplies(ctx, parentMessageID, &getstream.GetRepliesRequest{
    Limit: getstream.PtrTo(limit),
    IDLte: getstream.PtrTo("42"),
    Sort:  []getstream.SortParamRequest{},
})
```

</codetabs-item>

<codetabs-item value="csharp" label="C#">

```csharp
// 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" });
```

</codetabs-item>

</codetabs>

## 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`.

<codetabs>

<codetabs-item value="javascript" label="JavaScript">

```js
const message = await channel.sendMessage({
  text: "I agree with this point",
  quoted_message_id: originalMessageId,
});
```

</codetabs-item>

<codetabs-item value="kotlin" label="Kotlin">

```kotlin
val message = Message(
  text = "I agree with this point",
  replyMessageId = originalMessage.id,
)
channelClient.sendMessage(message).enqueue { /* ... */ }
```

</codetabs-item>

<codetabs-item value="swift" label="Swift">

```swift
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)
  }
}
```

</codetabs-item>

<codetabs-item value="dart" label="Dart">

```dart
final message = await channel.sendMessage(Message(
  text: "I agree with this point",
  quotedMessageId: originalMessageId,
));
```

</codetabs-item>

<codetabs-item value="java" label="Java">

```java
// Android SDK
Message message = new Message();
message.setText("I agree with this point");
message.setReplyMessageId(originalMessage.getId());

channelClient.sendMessage(message).enqueue(result -> { /* ... */ });

// Backend SDK
chat.sendMessage(type, id, SendMessageRequest.builder()
    .message(MessageRequest.builder()
        .text("I agree with this point")
        .quotedMessageID(originalMessageId)
        .userID(userId)
        .build())
    .build()).execute();
```

</codetabs-item>

<codetabs-item value="python" label="Python">

```python
from getstream.models import MessageRequest

channel.send_message(
    MessageRequest(
        text="I agree with this point",
        quoted_message_id=original_message_id,
        user_id=user_id,
    )
)
```

</codetabs-item>

<codetabs-item value="ruby" label="Ruby">

```ruby
require 'getstream_ruby'
Models = GetStream::Generated::Models

client.chat.send_message('messaging', channel_id, Models::SendMessageRequest.new(
  message: Models::MessageRequest.new(
    text: 'I agree with this point',
    quoted_message_id: original_message_id,
    user_id: user_id
  )
))
```

</codetabs-item>

<codetabs-item value="go" label="Go">

```go
resp, err := channel.SendMessage(ctx, &getstream.SendMessageRequest{
    Message: getstream.MessageRequest{
        Text:             getstream.PtrTo("I agree with this point"),
        QuotedMessageID:  getstream.PtrTo(originalMessageID),
        UserID:           getstream.PtrTo(userID),
    },
})
```

</codetabs-item>

<codetabs-item value="csharp" label="C#">

```csharp
var resp = await chat.SendMessageAsync("messaging", channelId,
    new SendMessageRequest
    {
        Message = new MessageRequest
        {
            Text = "I agree with this point",
            UserID = userId,
            QuotedMessageID = originalMessageId
        }
    });
```

</codetabs-item>

<codetabs-item value="unity" label="Unity">

```csharp
var message = await channel.SendNewMessageAsync(new StreamSendMessageRequest
{
  QuotedMessage = originalMessage,
  Text = "I agree with this point",
});
```

</codetabs-item>

</codetabs>

When querying messages, the `quoted_message` field is automatically populated:

```json
{
  "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"
  }
}
```

<admonition type="warning">

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.

</admonition>

## 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.

<codetabs>

<codetabs-item value="javascript" label="JavaScript">

```js
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);
}
```

</codetabs-item>

<codetabs-item value="kotlin" label="Kotlin">

```kotlin
val request = QueryThreadsRequest()
client.queryThreadsResult(request).enqueue { result ->
  if (result is Result.Success) {
    val threads: List<Thread> = result.value.threads
  } else {
    // Handle Result.Failure
  }
}
```

</codetabs-item>

<codetabs-item value="swift" label="Swift">

```swift
let query = ThreadListQuery(watch: true)
let threadListController = chatClient.threadListController(query: query)
threadListController.synchronize { error in
  print(threadListController.threads)
}
```

</codetabs-item>

</codetabs>

<disclosure label="Example Response">

```json
{
  "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
        }
      ]
    }
  ]
}
```

</disclosure>

### 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_count`
- `created_at`
- `last_message_at`
- `parent_message_id`
- `participant_count`
- `reply_count`
- `updated_at`

Use `1` for ascending order and `-1` for descending order.

<codetabs>

<codetabs-item value="javascript" label="JavaScript">

```js
// 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,
});
```

</codetabs-item>

<codetabs-item value="kotlin" label="Kotlin">

```kotlin
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()
```

</codetabs-item>

<codetabs-item value="swift" label="Swift">

```swift
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
}
```

</codetabs-item>

<codetabs-item value="python" label="Python">

```python
from getstream.models import SortParamRequest

filter_conditions = {
    "created_by_user_id": {"$eq": "user-1"},
    "updated_at": {"$gte": "2024-01-01T00:00:00Z"},
}
sort = [SortParamRequest(field="created_at", direction=-1)]

response = client.chat.query_threads(
    filter=filter_conditions,
    sort=sort,
    limit=10,
    user_id=user_id,
)

# Get next page
if response.data.next:
    next_page = client.chat.query_threads(
        filter=filter_conditions,
        sort=sort,
        limit=10,
        user_id=user_id,
        next=response.data.next,
    )
```

</codetabs-item>

<codetabs-item value="php" label="PHP">

```php
$response = $client->queryThreads(new Models\QueryThreadsRequest(
    filter: (object)[
        "created_by_user_id" => (object)['$eq' => "user-1"],
        "updated_at" => (object)['$gte' => "2024-01-01T00:00:00Z"],
    ],
    sort: [new Models\SortParamRequest(field: "created_at", direction: -1)],
    userID: $userId,
    limit: 10,
));

// Get next page
if ($response->getData()->next) {
    $nextPage = $client->queryThreads(new Models\QueryThreadsRequest(
        filter: (object)[
            "created_by_user_id" => (object)['$eq' => "user-1"],
            "updated_at" => (object)['$gte' => "2024-01-01T00:00:00Z"],
        ],
        sort: [new Models\SortParamRequest(field: "created_at", direction: -1)],
        userID: $userId,
        limit: 10,
        next: $response->getData()->next,
    ));
}
```

</codetabs-item>

<codetabs-item value="ruby" label="Ruby">

```ruby
require 'getstream_ruby'
Models = GetStream::Generated::Models

filter = {
  'created_by_user_id' => { '$eq' => 'user-1' },
  'updated_at' => { '$gte' => '2024-01-01T00:00:00Z' }
}
sort = [Models::SortParamRequest.new(field: 'created_at', direction: -1)]

response = client.chat.query_threads(Models::QueryThreadsRequest.new(
  filter: filter,
  sort: sort,
  limit: 10,
  user_id: user_id
))

# Get next page
if response.next
  next_page = client.chat.query_threads(Models::QueryThreadsRequest.new(
    filter: filter,
    sort: sort,
    limit: 10,
    user_id: user_id,
    next: response.next
  ))
end
```

</codetabs-item>

<codetabs-item value="go" label="Go">

```go
limit := 10
resp, err := client.Chat().QueryThreads(ctx, &getstream.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: []getstream.SortParamRequest{
        {Field: getstream.PtrTo("created_at"), Direction: getstream.PtrTo(-1)},
    },
    Limit:  getstream.PtrTo(limit),
    UserID: getstream.PtrTo(userID),
})

// Get next page
if resp.Data.Next != nil {
    nextPage, err := client.Chat().QueryThreads(ctx, &getstream.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: []getstream.SortParamRequest{
            {Field: getstream.PtrTo("created_at"), Direction: getstream.PtrTo(-1)},
        },
        Limit:  getstream.PtrTo(limit),
        UserID: getstream.PtrTo(userID),
        Next:   resp.Data.Next,
    })
}
```

</codetabs-item>

<codetabs-item value="csharp" label="C#">

```csharp
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
    });
}
```

</codetabs-item>

<codetabs-item value="java" label="Java">

```java
Map<String, Object> filter = Map.of(
    "created_by_user_id", Map.of("$eq", "user-1"),
    "updated_at", Map.of("$gte", "2024-01-01T00:00:00Z")
);

List<SortParamRequest> sort = List.of(
    SortParamRequest.builder().field("created_at").direction(-1).build()
);

var response = chat.queryThreads(QueryThreadsRequest.builder()
    .userID(userId)
    .filter(filter)
    .sort(sort)
    .limit(10)
    .build()).execute().getData();

// Get next page
if (response.getNext() != null) {
    var nextPage = chat.queryThreads(QueryThreadsRequest.builder()
        .userID(userId)
        .filter(filter)
        .sort(sort)
        .limit(10)
        .next(response.getNext())
        .build()).execute().getData();
}
```

</codetabs-item>

</codetabs>

### Getting a Thread by ID

Retrieve a specific thread using the parent message ID.

<codetabs>

<codetabs-item value="javascript" label="JavaScript">

```js
const thread = await client.getThread(parentMessageId, {
  watch: true,
  reply_limit: 10,
  participant_limit: 25,
});
```

</codetabs-item>

<codetabs-item value="kotlin" label="Kotlin">

```kotlin
val options = GetThreadOptions(
  watch = true,
  replyLimit = 10,
  participantLimit = 25,
)

client.getThread(parentMessageId, options).enqueue { /* ... */ }
```

</codetabs-item>

</codetabs>

### Updating Thread Title and Custom Data

Assign a title and custom data to a thread.

<codetabs>

<codetabs-item value="javascript" label="JavaScript">

```js
// Set properties
const { thread } = await client.partialUpdateThread(threadId, {
  set: {
    title: "Project Discussion",
    priority: "high",
  },
});

// Remove properties
await client.partialUpdateThread(threadId, {
  unset: ["priority"],
});
```

</codetabs-item>

<codetabs-item value="kotlin" label="Kotlin">

```kotlin
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
    }
}
```

</codetabs-item>

<codetabs-item value="swift" label="Swift">

```swift
// 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
  }
```

</codetabs-item>

</codetabs>

## Thread Unread Counts

### Total Unread Threads

The total number of unread threads is available after connecting.

<codetabs>

<codetabs-item value="javascript" label="JavaScript">

```js
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();
```

</codetabs-item>

<codetabs-item value="swift" label="Swift">

```swift
let connectedUser = try await chatClient.connectUser(
  userInfo: .init(id: "user-id"),
  token: token
)
print(connectedUser.state.user.unreadCount.threads)
```

</codetabs-item>

</codetabs>

### Marking Threads as Read or Unread

<codetabs>

<codetabs-item value="javascript" label="JavaScript">

```js
// Mark thread as read
await channel.markRead({ thread_id: parentMessageId });

// Mark thread as unread
await channel.markUnread({ thread_id: parentMessageId });
```

</codetabs-item>

<codetabs-item value="swift" label="Swift">

```swift
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
}
```

</codetabs-item>

</codetabs>

### Unread Count Per Thread

<codetabs>

<codetabs-item value="javascript" label="JavaScript">

```js
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);
}
```

</codetabs-item>

</codetabs>

## Thread Manager

The `ThreadManager` class provides built-in pagination and state management for threads.

```js
// 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.

```js
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);
});
```

<admonition type="info">

The `watch` parameter is required when querying threads to receive real-time updates.

</admonition>

For `ThreadManager`, call `registerSubscriptions` once to automatically manage subscriptions for all loaded threads:

```js
const threadManager = client.threads;
threadManager.registerSubscriptions();

await threadManager.loadNextPage();

// All threads are now listening to channel events
const { threads } = threadManager.state.getLatestValue();
```


---

This page was last updated at 2026-03-13T13:16:55.317Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/react-native/threads/](https://getstream.io/chat/docs/react-native/threads/).