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

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. Users are automatically subscribed to a thread 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. Let's see how to work with thread subscriptions in CometChat's iOS SDK.

<Note>
  **Available from Chat SDK v4.1.9.** These APIs require `CometChatSDK` v4.1.9 or later.

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

## 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="Swift">
    ```swift theme={null}
    let parentMessageId = 1

    CometChat.subscribeToThread(parentMessageId: parentMessageId) { response in
        print("Subscribed to thread: \(response)")
    } onError: { error in
        print("Failed to subscribe: \(error.errorDescription)")
    }
    ```
  </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="Swift">
    ```swift theme={null}
    let parentMessageId = 1

    CometChat.unsubscribeFromThread(parentMessageId: parentMessageId) { response in
        print("Unsubscribed from thread: \(response)")
    } onError: { error in
        print("Failed to unsubscribe: \(error.errorDescription)")
    }
    ```
  </Tab>
</Tabs>

<Warning>
  Unsubscribing is **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

Every fetched message carries the logged-in user's subscription state for its own thread on `BaseMessage.threadSubscribed`. It arrives with the message fetch, so rendering a subscribe control needs **no extra network call** and no separate state cache.

<Tabs>
  <Tab title="Swift">
    ```swift theme={null}
    if message.threadSubscribed {
        // render "Unsubscribe from thread"
    } else {
        // render "Subscribe to thread"
    }
    ```
  </Tab>
</Tabs>

Because it is a plain `Bool`, read it only off a message you actually fetched:

| Situation                                    | `threadSubscribed` | How to treat it                               |
| -------------------------------------------- | ------------------ | --------------------------------------------- |
| Message fetched via `MessagesRequest`        | Authoritative      | Render the state directly.                    |
| Message that arrived live over the websocket | `false`            | Means **"not reported"**, not "unsubscribed". |
| Message you constructed locally              | `false`            | Not yet known.                                |

<Warning>
  A `false` on a message that was **not** fetched with the flag means "the server did not tell me", not "the user is unsubscribed". Only a fetched flag is authoritative. Render the unsubscribed state (an enabled "Subscribe" control) in the unknown case — never a spinner or a disabled control.
</Warning>

The property also has a setter, which is **local only and performs no network call**. It exists so you can align message objects you already hold with a truth you have just established — for example after a successful `subscribeToThread`:

<Tabs>
  <Tab title="Swift">
    ```swift theme={null}
    CometChat.subscribeToThread(parentMessageId: parentMessageId) { _ in
        message.threadSubscribed = true   // local only; keeps your held objects in step
    } onError: { error in
        print("Failed to subscribe: \(error.errorDescription)")
    }
    ```
  </Tab>
</Tabs>

To change the actual subscription, always use `subscribeToThread` / `unsubscribeFromThread`.

## 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                                                                                                    |
| ------------------------ | -------------------------------------------------------------------------------------------------------------- |
| `set(limit:)`            | Page size. Thread rows are heavy — each carries a root message and a last reply.                               |
| `set(uid:)`              | Scope the list to threads in the one-on-one conversation with this user. Mutually exclusive with `set(guid:)`. |
| `set(guid:)`             | Scope the list to threads in this group. Mutually exclusive with `set(uid:)`.                                  |
| `set(participatedByMe:)` | Defaults to `true`. Only the threads the logged-in user participates in are returned.                          |

<Tabs>
  <Tab title="Swift">
    ```swift theme={null}
    let threadsRequest = ThreadsRequest.ThreadsRequestBuilder()
        .set(limit: 30)
        .build()

    threadsRequest.fetchNext { threads in
        for thread in threads {
            print("Thread \(thread.parentMessageId) has \(thread.replyCount) replies")
        }
    } onError: { error in
        print("Threads fetch failed: \(error.errorDescription)")
    }
    ```
  </Tab>
</Tabs>

Call `fetchNext` repeatedly to page forward; `hasMore()` tells you whether more pages exist. A `ThreadsRequest` is **single-use and 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.

Paging is internally keyed on a compound **`(updatedAt, id)`** cursor, matching the JS and Android SDKs. `updatedAt` alone is second-granular, so threads updated within the same second could not be separated by it and a page boundary could only step past the whole second — skipping every unseen row in it. Carrying the boundary row's id makes the cursor address one exact row, so a block of same-second threads spills across pages instead of being dropped. You do not set this yourself; it matters only if you were previously working around duplicated or skipped rows.

### The MessageThread Model

Each row is a `MessageThread`:

| Property           | Description                                                                                                                                |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `parentMessageId`  | The thread's identity — the ID of its root message.                                                                                        |
| `parentMessage`    | The root message as a full `BaseMessage`.                                                                                                  |
| `replyCount`       | Number of replies in the thread.                                                                                                           |
| `lastReply`        | The most recent reply as a `BaseMessage`. `nil` for a thread with no replies yet — expected, not an error.                                 |
| `conversationId`   | The ID of the conversation the thread belongs to.                                                                                          |
| `receiverType`     | `user` or `group`.                                                                                                                         |
| `receiverUid`      | The raw `UID`/`GUID` of the conversation. Resolve the display name and avatar yourself via `CometChat.getUser()` / `CometChat.getGroup()`. |
| `isSubscribed`     | Always `true` for rows in this list — presence in the list *is* the subscription.                                                          |
| `unreadReplyCount` | Reserved for future use — currently `nil` (unknown), which is not the same as `0`.                                                         |
| `updatedAt`        | Part of the internal pagination cursor (paired with the row's id). Second-granular. **Do not sort your UI on it.**                         |

<Warning>
  To order rows in your UI, sort on `lastReply?.sentAt`, falling back to `parentMessage?.sentAt` for zero-reply threads — not on `updatedAt`.
</Warning>

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

## Keeping Your UI in Sync

The SDK does **not** expose a thread listener or thread-subscription callbacks. It deliberately does not cache subscription state: the state it could not fully observe drifted, so the server's flag on the message is the single source of truth.

Keep your UI in step yourself:

* **After your own subscribe/unsubscribe** — update the message you hold by setting `threadSubscribed` in the success callback (local only, no network). This is the normal case and needs nothing else.
* **On the next fetch** — `threadSubscribed` arrives with every fetched message, so a refresh always corrects the state.
* **For new replies** — use the regular message listener (`onTextMessageReceived` and friends) and check `parentMessageId` to spot a threaded reply.

<Note>
  A subscribe or unsubscribe performed on the user's **other device** produces no real-time event on this one. The state self-corrects on the next message fetch, so refresh when the app returns to the foreground.
</Note>

<Warning>
  Do not build a long-lived local cache of subscription state keyed by parent message id. That is exactly the design the SDK moved away from — it cannot observe every change, so it drifts. Read `threadSubscribed` off the message each time you render.
</Warning>

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

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

### Quoted replies are a separate preference

A **quoted reply** (a reply to one specific message) and a **threaded reply** (a message posted into a thread) are configured independently, through two different enums. They are not interchangeable — the raw value `4` means something different on each.

| Enum                   | Set with                        | Fourth option                         |
| ---------------------- | ------------------------------- | ------------------------------------- |
| `RepliesOptions`       | `set(repliesPreference:)`       | `SUBSCRIBE_TO_SUBSCRIBED_THREADS`     |
| `QuotedRepliesOptions` | `set(quotedRepliesPreference:)` | `SUBSCRIBE_TO_QUOTES_ON_OWN_MESSAGES` |

<Tabs>
  <Tab title="Swift">
    ```swift theme={null}
    let groupPreferences = CometChatNotifications.GroupPreferences()

    // Threaded replies: only threads this user follows.
    groupPreferences.set(repliesPreference: .SUBSCRIBE_TO_SUBSCRIBED_THREADS)

    // Quoted replies: only quotes of this user's own messages.
    groupPreferences.set(quotedRepliesPreference: .SUBSCRIBE_TO_QUOTES_ON_OWN_MESSAGES)
    ```
  </Tab>
</Tabs>

Both preferences exist on group and one-on-one preferences alike.

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

## Error Handling

| Error                      | Meaning                                                                                                                                                                  |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `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).                                                                                                         |
