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

<Tabs>

```kotlin label="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
  }
}
```

```java label="Java (Android)"
Message message = new Message.Builder()
    .withText("This is a reply in a thread")
    .withParentId(parentMessage.getId())
    .build();

channelClient.sendMessage(message).enqueue(result -> {
  if (result.isSuccess()) {
    Message sentMessage = result.getOrNull();
  } else {
    // Handle error
  }
});
```

</Tabs>

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

<Tabs>

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

```java label="Java (Android)"
int limit = 20;

// Get the first 20 replies
client.getReplies(parentMessage.getId(), limit).enqueue(result -> {
  if (result.isSuccess()) {
    List<Message> replies = result.getOrNull();
  } else {
    // Handle error
  }
});

// Get 20 more replies before message with ID "42"
client.getRepliesMore(parentMessage.getId(), "42", limit).enqueue(result -> { /* ... */ });
```

</Tabs>

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

<Tabs>

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

```java label="Java (Android)"
Message message = new Message.Builder()
    .withText("I agree with this point")
    .withReplyMessageId(originalMessage.getId())
    .build();

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

</Tabs>

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.

```kotlin label="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
  }
}
```

<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](https://getstream.io/docs/platform/query-syntax-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.

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

### Getting a Thread by ID

Retrieve a specific thread using the parent message ID.

```kotlin label="Kotlin"
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.

```kotlin label="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
    }
}
```

## Thread Unread Counts

### Total Unread Threads

The total number of unread threads is available after connecting.

```js label="JavaScript"
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();
```

### Marking Threads as Read or Unread

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

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

### Unread Count Per Thread

```js label="JavaScript"
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.

```js label="JavaScript"
// 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-09-08T17:14:05.346Z.

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