> ## Documentation Index
> Fetch the complete documentation index at: https://www.cometchat.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Thread Subscription

> Subscribe and unsubscribe from message threads, read subscription state off a message, and fetch participated threads with the CometChat Android SDK.

<Accordion title="AI Integration Quick Reference">
  ```kotlin theme={null}
  // Subscribe / unsubscribe — a thread is identified by its PARENT message id
  CometChat.subscribeToThread(parentMessageId, callbackListener)
  CometChat.unsubscribeFromThread(parentMessageId, callbackListener)

  // Read state off a fetched parent message — there is no state store to query
  val following: Boolean = parentMessage.isThreadSubscribed

  // Align local copies after the server accepts (LOCAL ONLY — sends nothing)
  parentMessage.setThreadSubscribed(true)

  // Thread inbox
  val request = ThreadsRequest.ThreadsRequestBuilder()
      .setParticipatedByMe(true)
      .setLimit(30)
      .build()
  request.fetchNext(callbackListener)          // List<MessageThread>
  ```
</Accordion>

Give users Slack-style control over thread noise. A user can **subscribe** to a message thread to be notified of future replies, or **unsubscribe** from it to mute it.

The server subscribes a user to a thread automatically when they start it, reply in it, or are @-mentioned in it — and they can explicitly subscribe to any parent message, even one that has no replies yet. The SDK also exposes the list of threads a user participates in, so you can build a thread inbox.

<Note>
  Thread subscription builds on [Threaded Messages](/docs/sdk/android/v5/threaded-messages). A thread is identified by the ID of its **parent message** — there is no separate thread ID.
</Note>

## How State Works

The SDK keeps **no subscription state of its own**. There is no cache and no thread listener to reconcile:

* Every message fetch asks the server for the flag, and it arrives on the message — read it with `BaseMessage.isThreadSubscribed()`.
* `subscribeToThread()` and `unsubscribeFromThread()` call back when the server has accepted the change. **The callback is the acknowledgement** — there is no follow-up event.

Your app owns the resulting UI state. That means you decide when to flip a toggle optimistically, and you decide what a thread's state is before you have fetched it.

## Subscribe to a Thread

To subscribe to a thread, use the `subscribeToThread` method with the ID of the thread's parent message. The call is **idempotent** — subscribing to a thread the user is already subscribed to succeeds silently. Subscribing to a message with zero replies is allowed; the user will be notified when the first reply arrives.

<Tabs>
  <Tab title="Java">
    ```java theme={null}
    long parentMessageId = 1;

    CometChat.subscribeToThread(parentMessageId, new CometChat.CallbackListener<String>() {
      @Override
      public void onSuccess(String response) {
          // The server has accepted it — flip your toggle here.
          Log.d(TAG, "Subscribed to thread: " + response);
      }

      @Override
      public void onError(CometChatException e) {
          Log.e(TAG, "Failed to subscribe: " + e.getMessage());
      }
    });
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    val parentMessageId = 1L

    CometChat.subscribeToThread(parentMessageId, object : CometChat.CallbackListener<String>() {
      override fun onSuccess(response: String?) {
          // The server has accepted it — flip your toggle here.
          Log.d(TAG, "Subscribed to thread: $response")
      }

      override fun onError(e: CometChatException?) {
          Log.e(TAG, "Failed to subscribe: ${e?.message}")
      }
    })
    ```
  </Tab>
</Tabs>

## Unsubscribe from a Thread

To unsubscribe from a thread, use the `unsubscribeFromThread` method. This too is idempotent — unsubscribing from a thread the user is not subscribed to succeeds silently.

<Tabs>
  <Tab title="Java">
    ```java theme={null}
    long parentMessageId = 1;

    CometChat.unsubscribeFromThread(parentMessageId, new CometChat.CallbackListener<String>() {
      @Override
      public void onSuccess(String response) {
          Log.d(TAG, "Unsubscribed from thread: " + response);
      }

      @Override
      public void onError(CometChatException e) {
          Log.e(TAG, "Failed to unsubscribe: " + e.getMessage());
      }
    });
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    val parentMessageId = 1L

    CometChat.unsubscribeFromThread(parentMessageId, object : CometChat.CallbackListener<String>() {
      override fun onSuccess(response: String?) {
          Log.d(TAG, "Unsubscribed from thread: $response")
      }

      override fun onError(e: CometChatException?) {
          Log.e(TAG, "Failed to unsubscribe: ${e?.message}")
      }
    })
    ```
  </Tab>
</Tabs>

<Warning>
  Unsubscribing **hard-deletes** the server row, so a thread inbox built with `ThreadsRequest` must drop that row rather than mark it unfollowed. It is also **not sticky**: if the user replies in the thread again, or is @-mentioned in it, they are automatically re-subscribed. Do not promise users "you won't be notified about this thread again".
</Warning>

## Read the Subscription State

The subscription state rides on the message. Read it with `isThreadSubscribed()` on the thread's **parent** message.

<Tabs>
  <Tab title="Java">
    ```java theme={null}
    if (parentMessage.isThreadSubscribed()) {
        // render "Unsubscribe from thread"
    } else {
        // render "Subscribe to thread"
    }
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    if (parentMessage.isThreadSubscribed) {
        // render "Unsubscribe from thread"
    } else {
        // render "Subscribe to thread"
    }
    ```
  </Tab>
</Tabs>

The flag is populated only on responses to requests that asked for it. On `MessagesRequestBuilder` that opt-in is `withThreadSubscribed(boolean)`, and it **defaults to `true`** — leave it there. Passing `false` trades the signal away for a marginally smaller response, and `isThreadSubscribed()` then reads `false` for every message, indistinguishable from a genuine unsubscribe.

```kotlin theme={null}
val messagesRequest = MessagesRequest.MessagesRequestBuilder()
  .setUID(UID)
  .setLimit(50)
  .withThreadSubscribed(true)   // the default — shown here for clarity
  .build()
```

<Note>
  A message delivered over the **websocket** carries no flag and therefore reads `false`. That is not a claim that the user is unsubscribed — it means nobody asked. When you need certainty for a thread you have not fetched (a deep link, for instance), fetch the parent message with `CometChat.getMessageDetails()` and read the flag off the result.
</Note>

### You are subscribed to your own messages

Sending a message subscribes you to the thread it may later grow — there is nothing to call. The message comes back with the flag set, both in the send response and on later fetches, and **only for you**: the flag is per-viewer, so the same message reads `false` for everybody else until they subscribe themselves.

That default is what makes the flag meaningful on your own messages. Since it starts out `true`, a `false` on a message **you sent** — read from a fetch, not the socket — is not silence. It means you unsubscribed, and nothing should quietly put you back.

This only holds for a message you sent and obtained from a fetch. On anyone else's message, or on anything socket-delivered, `false` still just means the server was not asked.

### Keeping your own copies in sync

The same thread can be represented by several message objects at once — a row in the message list, the parent of an open thread view, an entry in a thread inbox. Because the SDK caches nothing, use `setThreadSubscribed()` to align the copies you hold once you know the answer:

<Tabs>
  <Tab title="Java">
    ```java theme={null}
    CometChat.subscribeToThread(parentMessageId, new CometChat.CallbackListener<String>() {
      @Override
      public void onSuccess(String response) {
          // The server accepted it — bring the objects you are rendering into line.
          parentMessage.setThreadSubscribed(true);
      }

      @Override
      public void onError(CometChatException e) { }
    });
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    CometChat.subscribeToThread(parentMessageId, object : CometChat.CallbackListener<String>() {
      override fun onSuccess(response: String?) {
          // The server accepted it — bring the objects you are rendering into line.
          parentMessage.isThreadSubscribed = true
      }

      override fun onError(e: CometChatException?) { }
    })
    ```
  </Tab>
</Tabs>

<Warning>
  `setThreadSubscribed()` is **local only** — it changes the object in memory and sends nothing to the server. Use `subscribeToThread()` / `unsubscribeFromThread()` to change the actual subscription.
</Warning>

## Reacting to Replies

A thread reply is an **ordinary message** with a parent message ID set, delivered through the standard `MessageListener` like any other message. There is no separate thread listener.

<Tabs>
  <Tab title="Java">
    ```java theme={null}
    private String listenerID = "UNIQUE_LISTENER_ID";

    CometChat.addMessageListener(listenerID, new CometChat.MessageListener() {
      @Override
      public void onTextMessageReceived(TextMessage message) {
          long parentMessageId = message.getParentMessageId();
          if (parentMessageId > 0) {
              // A reply landed in a thread — bump your thread row here.
          }
      }
    });
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    val listenerID = "UNIQUE_LISTENER_ID"

    CometChat.addMessageListener(listenerID, object : CometChat.MessageListener() {
      override fun onTextMessageReceived(message: TextMessage) {
          if (message.parentMessageId > 0) {
              // A reply landed in a thread — bump your thread row here.
          }
      }
    })
    ```
  </Tab>
</Tabs>

To stop listening, remove the listener with `CometChat.removeMessageListener(listenerID)`.

Using `MessageListener` also gets you `onMessageEdited` and `onMessageDeleted` for replies, which a thread-only channel would not. Your own replies do not arrive on a listener — bump your thread row from the `sendMessage()` callback instead.

## Fetch the Threads a User Participates In

To build a thread inbox — one row per thread the user is part of — create a `ThreadsRequest` using the `ThreadsRequestBuilder`. The list is the union of threads the user started, replied in, was mentioned in, or explicitly subscribed to. Every returned row is, by definition, a thread the user is subscribed to: **participation is subscription**, and unsubscribing removes the row.

| Setting                        | Description                                                                                                                                   |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `setLimit(int value)`          | Page size, validated between 1 and 1000 at fetch time. Defaults to 30 — thread rows are heavy (each carries a root message and a last reply). |
| `setUid(String value)`         | Scope the list to threads in the one-on-one conversation with this user. Mutually exclusive with `setGuid()`.                                 |
| `setGuid(String value)`        | Scope the list to threads in this group. Mutually exclusive with `setUid()`.                                                                  |
| `setParticipatedByMe(boolean)` | Defaults to `true`. Only the threads the logged-in user participates in are returned.                                                         |

<Tabs>
  <Tab title="Java">
    ```java theme={null}
    ThreadsRequest threadsRequest = new ThreadsRequest.ThreadsRequestBuilder()
      .setParticipatedByMe(true)
      .setLimit(30)
      .build();

    threadsRequest.fetchNext(new CometChat.CallbackListener<List<MessageThread>>() {
      @Override
      public void onSuccess(List<MessageThread> threads) {
          for (MessageThread thread : threads) {
              Log.d(TAG, "Thread " + thread.getParentMessageId()
                      + " has " + thread.getReplyCount() + " replies");
          }
      }

      @Override
      public void onError(CometChatException e) {
          Log.e(TAG, "Threads fetch failed: " + e.getMessage());
      }
    });
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    val threadsRequest = ThreadsRequest.ThreadsRequestBuilder()
      .setParticipatedByMe(true)
      .setLimit(30)
      .build()

    threadsRequest.fetchNext(object : CometChat.CallbackListener<List<MessageThread>>() {
      override fun onSuccess(threads: List<MessageThread>?) {
          threads?.forEach { thread ->
              Log.d(TAG, "Thread ${thread.parentMessageId} has ${thread.replyCount} replies")
          }
      }

      override fun onError(e: CometChatException?) {
          Log.e(TAG, "Threads fetch failed: ${e?.message}")
      }
    })
    ```
  </Tab>
</Tabs>

Call `fetchNext()` repeatedly to page forward; `hasMore()` tells you whether more pages exist. A `ThreadsRequest` is **forward-only** — there is no `fetchPrevious()`. To refresh the list from the top, build a new request from the builder and replace your list with its results. Calling `fetchNext()` while a fetch is already in flight fails with a request-in-progress error, and a `setLimit()` outside 1…1000 fails at fetch time rather than at `build()`.

<Warning>
  `ThreadsRequestBuilder` spells these `setUid()` / `setGuid()` — **not** the `setUID()` / `setGUID()` used by `MessagesRequestBuilder`. The two are mutually exclusive; setting both throws an `IllegalArgumentException` from `build()`.
</Warning>

### The MessageThread Model

Each row is a `MessageThread` — enough to render an inbox without fetching the parent message separately.

| Method                  | Description                                                                                                                              |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `getParentMessageId()`  | The thread's identity — the ID of its root message. Pass it to `subscribeToThread()`.                                                    |
| `getParentMessage()`    | The root message as a full `BaseMessage`.                                                                                                |
| `getReplyCount()`       | Number of replies in the thread.                                                                                                         |
| `getLastReply()`        | The most recent reply as a `BaseMessage`. `null` for a thread with no replies yet — expected, not an error.                              |
| `getConversationId()`   | The ID of the conversation the thread belongs to.                                                                                        |
| `getReceiverType()`     | `user` or `group`.                                                                                                                       |
| `getReceiverUid()`      | The raw `UID`/`GUID` of the conversation — no name or avatar. Resolve those yourself via `CometChat.getUser()` / `CometChat.getGroup()`. |
| `isSubscribed()`        | Always `true` for rows from a `setParticipatedByMe(true)` fetch — presence in the list *is* the subscription.                            |
| `getUnreadReplyCount()` | The unread reply count, or `null` when unknown. `null` is not `0`.                                                                       |
| `getUpdatedAt()`        | An opaque pagination cursor (unix seconds, no tiebreak). **Do not sort your UI on it.**                                                  |
| `getRawData()`          | The untouched raw thread object — the escape hatch for any field not yet modelled.                                                       |

<Warning>
  To order rows in your UI, sort on `getLastReply().getSentAt()`, falling back to `getParentMessage().getSentAt()` for zero-reply threads — not on `getUpdatedAt()`.
</Warning>

<Note>
  `getUnreadReplyCount()` returns `null`, not `0`, when the count is unknown. Treat `null` as "no badge" rather than as zero unread.
</Note>

<Info>
  The list starts **empty** for every user when the feature launches — it fills up as users reply, get mentioned, and subscribe to threads. There is no historical backfill.
</Info>

## Notification Preferences

The notification preference for replies gains a new value so users can be notified only for threads they are subscribed to: `SUBSCRIBE_TO_SUBSCRIBED_THREADS` in the `RepliesOptions` enum.

| Value                             | Behavior                                                        |
| --------------------------------- | --------------------------------------------------------------- |
| `DONT_SUBSCRIBE`                  | No notifications for thread replies.                            |
| `SUBSCRIBE_TO_ALL`                | Notifications for all thread replies.                           |
| `SUBSCRIBE_TO_MENTIONS`           | Notifications only for replies that mention the user.           |
| `SUBSCRIBE_TO_SUBSCRIBED_THREADS` | Notifications for replies in threads the user is subscribed to. |

See [Notification Preferences](/docs/notifications) for how to read and update a user's preferences.

## Error Handling

| Error                      | Meaning                                                                                                                                                                  |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ERROR_INVALID_MESSAGEID`  | The parent message ID was `0` or negative. The SDK rejects this locally, before any network call.                                                                        |
| `ERR_MESSAGE_NO_ACCESS`    | The user no longer has access to the message's conversation (for example, they left or were banned from the group). Treat the thread as inaccessible and remove its row. |
| `ERR_MESSAGE_ID_NOT_FOUND` | The parent message does not exist (for example, it was deleted).                                                                                                         |

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Threaded Messages" icon="comments" href="/docs/sdk/android/v5/threaded-messages">
    Send and fetch replies inside a thread
  </Card>

  <Card title="Mentions" icon="at" href="/docs/sdk/android/v5/mentions">
    @-mentions, which auto-subscribe a user to a thread
  </Card>

  <Card title="All Real Time Listeners" icon="tower-broadcast" href="/docs/sdk/android/v5/real-time-listeners">
    Every listener the SDK exposes, in one place
  </Card>

  <Card title="Save A Message" icon="bookmark" href="/docs/sdk/android/v5/save-message">
    Bookmark a message privately, across conversations
  </Card>
</CardGroup>
