diff --git a/docs.json b/docs.json
index cb43da08b..e45eaec60 100644
--- a/docs.json
+++ b/docs.json
@@ -3226,10 +3226,14 @@
"sdk/javascript/message-filtering",
"sdk/javascript/retrieve-conversations",
"sdk/javascript/threaded-messages",
+ "sdk/javascript/thread-subscription",
"sdk/javascript/edit-message",
"sdk/javascript/delete-message",
"sdk/javascript/flag-message",
+ "sdk/javascript/pin-message",
+ "sdk/javascript/save-message",
"sdk/javascript/delete-conversation",
+ "sdk/javascript/pin-conversation",
"sdk/javascript/typing-indicators",
"sdk/javascript/transient-messages",
"sdk/javascript/delivery-read-receipts",
diff --git a/sdk/javascript/all-real-time-listeners.mdx b/sdk/javascript/all-real-time-listeners.mdx
index bd78c0c1e..d24596d7f 100644
--- a/sdk/javascript/all-real-time-listeners.mdx
+++ b/sdk/javascript/all-real-time-listeners.mdx
@@ -3,14 +3,15 @@ title: "All Real Time Listeners"
description: "Use CometChat real-time listeners for user presence, groups, messages, calls, AI assistant events, and ongoing calls."
---
-CometChat provides 4 listeners viz.
+CometChat provides the following listeners.
1. [User Listener](/sdk/javascript/all-real-time-listeners#user-listener)
2. [Group Listener](/sdk/javascript/all-real-time-listeners#group-listener)
3. [Message Listener](/sdk/javascript/all-real-time-listeners#message-listener)
-4. [Call Listener](/sdk/javascript/all-real-time-listeners#call-listener)
-5. [AI Assistant Listener](/sdk/javascript/all-real-time-listeners#ai-assistant-listener)
-6. [Ongoing Call Listener (Calls SDK)](/sdk/javascript/all-real-time-listeners#ongoing-call-listener-calls-sdk)
+4. [Conversation Listener](/sdk/javascript/all-real-time-listeners#conversation-listener)
+5. [Call Listener](/sdk/javascript/all-real-time-listeners#call-listener)
+6. [AI Assistant Listener](/sdk/javascript/all-real-time-listeners#ai-assistant-listener)
+7. [Ongoing Call Listener (Calls SDK)](/sdk/javascript/all-real-time-listeners#ongoing-call-listener-calls-sdk)
## User Listener
@@ -308,6 +309,10 @@ Receive events for incoming messages, typing indicators, read/delivery receipts,
| **onMessagesDeliveredToAll(receipt: CometChat.MessageReceipt)** | This event is triggered when a group message is delivered to all members. Group conversations only. |
| **onMessagesReadByAll(receipt: CometChat.MessageReceipt)** | This event is triggered when a group message is read by all members. Group conversations only. |
| **onAIAssistantMessageReceived(message: CometChat.AIAssistantMessage)** | This event is triggered when a persisted AI assistant reply is received after an agent run completes. |
+| **onMessagePinned(message: CometChat.BaseMessage)** | This event is triggered when a message is pinned in a user/group conversation. Broadcast to everyone in that conversation. |
+| **onMessageUnpinned(message: CometChat.BaseMessage)** | This event is triggered when a message is unpinned in a user/group conversation. |
+| **onMessageSaved(message: CometChat.BaseMessage)** | This event is triggered when the logged-in user saves a message. Private and delivered to that user's other devices only. |
+| **onMessageUnsaved(message: CometChat.BaseMessage)** | This event is triggered when the logged-in user unsaves a message. |
| **onAIToolResultReceived(message: CometChat.AIToolResultMessage)** | This event is triggered when a persisted AI tool result message is received after an agent run completes. |
| **onAIToolArgumentsReceived(message: CometChat.AIToolArgumentMessage)** | This event is triggered when a persisted AI tool argument message is received after an agent run completes. |
| **onCardMessageReceived(message: CometChat.CardMessage)** | This event is triggered when a standalone [card message](/sdk/javascript/card-messages) is received. |
@@ -474,6 +479,77 @@ CometChat.removeMessageListener(listenerID);
+## Conversation Listener
+
+Receive events when a conversation is pinned or unpinned. This is a separate channel from `MessageListener` because its payload is a `Conversation`, not a message.
+
+| Method | Information |
+| ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
+| **onConversationPinned(conversation: CometChat.Conversation)** | Triggered when a conversation is pinned — by the logged-in user on another device, or globally by an admin. |
+| **onConversationUnpinned(conversation: CometChat.Conversation)** | Triggered when a conversation is unpinned. The `Conversation` arrives with its pin attributes cleared. |
+
+
+
+```typescript
+let listenerID: string = "UNIQUE_LISTENER_ID";
+
+CometChat.addConversationListener(
+ listenerID,
+ new CometChat.ConversationListener({
+ onConversationPinned: (conversation: CometChat.Conversation) => {
+ console.log("Conversation pinned", conversation);
+ },
+ onConversationUnpinned: (conversation: CometChat.Conversation) => {
+ console.log("Conversation unpinned", conversation);
+ },
+ })
+);
+```
+
+
+
+
+```javascript
+let listenerID = "UNIQUE_LISTENER_ID";
+
+CometChat.addConversationListener(
+ listenerID,
+ new CometChat.ConversationListener({
+ onConversationPinned: (conversation) => {
+ console.log("Conversation pinned", conversation);
+ },
+ onConversationUnpinned: (conversation) => {
+ console.log("Conversation unpinned", conversation);
+ },
+ })
+);
+```
+
+
+
+
+
+Remove the listener once you are done:
+
+
+
+```typescript
+CometChat.removeConversationListener("UNIQUE_LISTENER_ID");
+```
+
+
+
+
+```javascript
+CometChat.removeConversationListener("UNIQUE_LISTENER_ID");
+```
+
+
+
+
+
+See [Pin A Conversation](/sdk/javascript/pin-conversation) for the full feature.
+
## Call Listener
Receive events for incoming and outgoing call state changes.
diff --git a/sdk/javascript/message-filtering.mdx b/sdk/javascript/message-filtering.mdx
index 595b6964c..2c377e898 100644
--- a/sdk/javascript/message-filtering.mdx
+++ b/sdk/javascript/message-filtering.mdx
@@ -713,6 +713,101 @@ let messagesRequest = new CometChat.MessagesRequestBuilder()
The above code returns messages belonging to the thread with the specified parent message ID.
+## Pinned messages
+
+Use `setPinned(true)` to fetch only the pinned messages of a conversation. A pinned list is conversation-scoped, so pair it with `setUID()` or `setGUID()` — exactly one of the two is required.
+
+
+
+```typescript
+let UID: string = "UID",
+ limit: number = 50,
+ messagesRequest: CometChat.MessagesRequest =
+ new CometChat.MessagesRequestBuilder()
+ .setUID(UID)
+ .setLimit(limit)
+ .setPinned(true)
+ .build();
+```
+
+
+
+
+```javascript
+let UID = "UID";
+let limit = 50;
+let messagesRequest = new CometChat.MessagesRequestBuilder()
+ .setUID(UID)
+ .setLimit(limit)
+ .setPinned(true)
+ .build();
+```
+
+
+
+
+```typescript
+let GUID: string = "GUID",
+ limit: number = 50,
+ messagesRequest: CometChat.MessagesRequest =
+ new CometChat.MessagesRequestBuilder()
+ .setGUID(GUID)
+ .setLimit(limit)
+ .setPinned(true)
+ .build();
+```
+
+
+
+
+```javascript
+let GUID = "GUID";
+let limit = 50;
+let messagesRequest = new CometChat.MessagesRequestBuilder()
+ .setGUID(GUID)
+ .setLimit(limit)
+ .setPinned(true)
+ .build();
+```
+
+
+
+
+
+The list is ordered by pin time, most recently pinned first — not by when the messages were sent. See [Pin A Message](/sdk/javascript/pin-message) for the full feature.
+
+## Saved messages
+
+Use `setSaved(true)` to fetch the logged-in user's saved messages. The saved list is user-level and spans every conversation, so unlike the pinned list you do **not** set a UID or a GUID.
+
+
+
+```typescript
+let limit: number = 50,
+ messagesRequest: CometChat.MessagesRequest =
+ new CometChat.MessagesRequestBuilder()
+ .setLimit(limit)
+ .setSaved(true)
+ .build();
+```
+
+
+
+
+```javascript
+let limit = 50;
+let messagesRequest = new CometChat.MessagesRequestBuilder()
+ .setLimit(limit)
+ .setSaved(true)
+ .build();
+```
+
+
+
+
+
+Saves are private to the logged-in user. Because the rows come from different conversations, use `getConversationId()`, `getReceiverType()` and `getReceiverId()` on each message to route back to the right conversation. See [Save A Message](/sdk/javascript/save-message) for the full feature.
+
## Hide threaded messages in user/group conversations
Use `hideReplies(true)` to exclude threaded messages from the main conversation. Default is `false`.
diff --git a/sdk/javascript/pin-conversation.mdx b/sdk/javascript/pin-conversation.mdx
new file mode 100644
index 000000000..803dec74d
--- /dev/null
+++ b/sdk/javascript/pin-conversation.mdx
@@ -0,0 +1,284 @@
+---
+title: "Pin A Conversation"
+description: "Pin and unpin conversations, fetch the pinned conversation list, and listen for conversation pin events with the CometChat JavaScript SDK."
+---
+
+Pinning a conversation keeps it at the top of the logged-in user's conversation list. The pin is **private to that user** — nobody else sees it — and it syncs to their other devices.
+
+
+ This is separate from an **admin-global pin**, which is managed from the
+ [CometChat Dashboard](https://app.cometchat.com) and shows for every user. Those
+ cannot be created or removed from the SDK, only observed.
+
+
+## Pin a Conversation
+
+A conversation is addressed by its peer — the other user's UID for a one-on-one conversation, or the GUID for a group — together with the conversation type.
+
+
+
+ ```typescript
+ CometChat.pinConversation("cometchat-uid-1", CometChat.RECEIVER_TYPE.USER).then(
+ (conversation: CometChat.Conversation) => {
+ console.log("Conversation pinned:", conversation);
+ },
+ (error: CometChat.CometChatException) => {
+ console.log("Failed to pin conversation:", error);
+ }
+ );
+ ```
+
+
+ ```javascript
+ CometChat.pinConversation("cometchat-guid-1", CometChat.RECEIVER_TYPE.GROUP).then(
+ (conversation) => {
+ console.log("Conversation pinned:", conversation);
+ },
+ (error) => {
+ console.log("Failed to pin conversation:", error);
+ }
+ );
+ ```
+
+
+
+It resolves with the full updated `Conversation`, with `pinnedAt` and `pinnedBy` set. Pinning is idempotent.
+
+
+ Addressing by peer rather than by `conversationId` is deliberate: it lets you
+ pin a conversation that has no messages yet.
+
+
+## Unpin a Conversation
+
+
+
+ ```typescript
+ CometChat.unpinConversation(
+ "cometchat-uid-1",
+ CometChat.RECEIVER_TYPE.USER
+ ).then(
+ (conversation: CometChat.Conversation) => {
+ console.log("Conversation unpinned:", conversation);
+ },
+ (error: CometChat.CometChatException) => {
+ console.log("Failed to unpin conversation:", error);
+ }
+ );
+ ```
+
+
+ ```javascript
+ CometChat.unpinConversation(
+ "cometchat-uid-1",
+ CometChat.RECEIVER_TYPE.USER
+ ).then(
+ (conversation) => {
+ console.log("Conversation unpinned:", conversation);
+ },
+ (error) => {
+ console.log("Failed to unpin conversation:", error);
+ }
+ );
+ ```
+
+
+
+A user cannot unpin an admin-global pin — that call is rejected with `ERR_ACTION_NOT_ALLOWED`. Hide or disable the unpin control for conversations you know are system-pinned.
+
+## Fetch Pinned Conversations
+
+The default conversation list is already **pin-ordered** by the server: admin-global pins first, then the user's own pins, then everything else. Fetch it as you normally would and no extra filter is needed.
+
+To narrow the list to pins only, use `setPinnedBy()` with the tokens on `CometChat.PINNED_BY`.
+
+
+
+ ```typescript
+ let conversationsRequest: CometChat.ConversationsRequest =
+ new CometChat.ConversationsRequestBuilder()
+ .setPinnedBy([CometChat.PINNED_BY.SYSTEM, CometChat.PINNED_BY.ME])
+ .setLimit(30)
+ .build();
+
+ conversationsRequest.fetchNext().then(
+ (conversations: CometChat.Conversation[]) => {
+ console.log("Pinned conversations:", conversations);
+ },
+ (error: CometChat.CometChatException) => {
+ console.log("Failed to fetch conversations:", error);
+ }
+ );
+ ```
+
+
+ ```javascript
+ let conversationsRequest = new CometChat.ConversationsRequestBuilder()
+ .setPinnedBy([CometChat.PINNED_BY.ME])
+ .setLimit(30)
+ .build();
+
+ conversationsRequest.fetchNext().then(
+ (conversations) => {
+ console.log("Pinned conversations:", conversations);
+ },
+ (error) => {
+ console.log("Failed to fetch conversations:", error);
+ }
+ );
+ ```
+
+
+
+| Token | Meaning |
+| ------------------------- | ---------------------------------------------------- |
+| `CometChat.PINNED_BY.ME` | Conversations the logged-in user pinned themselves. |
+| `CometChat.PINNED_BY.SYSTEM` | Conversations pinned globally by an admin. |
+
+Passing an empty array is the same as not calling `setPinnedBy()` at all — you get the default, pin-ordered list.
+
+## Check if a Conversation is Pinned
+
+
+
+ ```typescript
+ if (conversation.isPinned()) {
+ console.log("Pinned at:", conversation.getPinnedAt());
+ console.log("Pinned by:", conversation.getPinnedBy());
+ }
+ ```
+
+
+ ```javascript
+ if (conversation.isPinned()) {
+ console.log("Pinned at:", conversation.getPinnedAt());
+ console.log("Pinned by:", conversation.getPinnedBy());
+ }
+ ```
+
+
+
+As with messages, the presence of `pinnedAt` *is* the boolean — an unpinned conversation has no pin attributes at all.
+
+## Real-time Conversation Pin Events
+
+Conversation pins arrive on a dedicated `ConversationListener`, **not** on `MessageListener` — the payload is a `Conversation`, not a message.
+
+
+
+ ```typescript
+ CometChat.addConversationListener(
+ "UNIQUE_LISTENER_ID",
+ new CometChat.ConversationListener({
+ onConversationPinned: (conversation: CometChat.Conversation) => {
+ console.log("Conversation pinned:", conversation);
+ },
+ onConversationUnpinned: (conversation: CometChat.Conversation) => {
+ console.log("Conversation unpinned:", conversation);
+ },
+ })
+ );
+ ```
+
+
+ ```javascript
+ CometChat.addConversationListener(
+ "UNIQUE_LISTENER_ID",
+ new CometChat.ConversationListener({
+ onConversationPinned: (conversation) => {
+ console.log("Conversation pinned:", conversation);
+ },
+ onConversationUnpinned: (conversation) => {
+ console.log("Conversation unpinned:", conversation);
+ },
+ })
+ );
+ ```
+
+
+
+These fire both when the logged-in user pins from another device and when an admin pins globally. Remove the listener when you are done:
+
+
+
+ ```typescript
+ CometChat.removeConversationListener("UNIQUE_LISTENER_ID");
+ ```
+
+
+ ```javascript
+ CometChat.removeConversationListener("UNIQUE_LISTENER_ID");
+ ```
+
+
+
+## Pin Limit
+
+
+
+ ```typescript
+ let limit: number | null = await CometChat.getPinnedConversationsLimit();
+ let systemLimit: number | null =
+ await CometChat.getSystemPinnedConversationsLimit();
+ ```
+
+
+ ```javascript
+ CometChat.getPinnedConversationsLimit().then((limit) => {
+ console.log("Pinned conversations limit:", limit);
+ });
+ ```
+
+
+
+Both resolve to `null` when the app settings carry no value. `getSystemPinnedConversationsLimit()` is the separate admin/global cap, enforced independently.
+
+## Feature Availability
+
+
+
+ ```typescript
+ let enabled: boolean = await CometChat.isPinConversationEnabled();
+
+ if (enabled) {
+ // Show the Pin Conversation option
+ }
+ ```
+
+
+ ```javascript
+ CometChat.isPinConversationEnabled().then((enabled) => {
+ if (enabled) {
+ // Show the Pin Conversation option
+ }
+ });
+ ```
+
+
+
+---
+
+## Next Steps
+
+
+
+ Fetch and order the conversation list
+
+
+ Highlight a message for everyone in a conversation
+
+
+ Bookmark a message privately, across conversations
+
+
+ Every listener the SDK exposes, in one place
+
+
diff --git a/sdk/javascript/pin-message.mdx b/sdk/javascript/pin-message.mdx
new file mode 100644
index 000000000..a22837e7e
--- /dev/null
+++ b/sdk/javascript/pin-message.mdx
@@ -0,0 +1,298 @@
+---
+title: "Pin A Message"
+description: "Pin and unpin messages in a conversation, fetch the pinned list, and listen for pin events with the CometChat JavaScript SDK."
+---
+
+Pinning highlights an important message in a conversation. A pin is **conversation-wide and visible to everyone** in that conversation, so it is the right tool for announcements, rules or a link everyone keeps asking for.
+
+
+ Pinning is a moderation action. Only an Admin or Moderator — a group owner
+ included — may pin or unpin. The server is the authority: a member's call is
+ rejected with `ERR_ACTION_NOT_ALLOWED`.
+
+
+## Pin a Message
+
+Call `pinMessage()` with the message's ID. It resolves with the full updated message, with `pinnedAt` and `pinnedBy` set.
+
+
+
+ ```typescript
+ let messageId: number = 100;
+
+ CometChat.pinMessage(messageId).then(
+ (message: CometChat.BaseMessage) => {
+ console.log("Message pinned:", message);
+ },
+ (error: CometChat.CometChatException) => {
+ console.log("Failed to pin message:", error);
+ }
+ );
+ ```
+
+
+ ```javascript
+ let messageId = 100;
+
+ CometChat.pinMessage(messageId).then(
+ (message) => {
+ console.log("Message pinned:", message);
+ },
+ (error) => {
+ console.log("Failed to pin message:", error);
+ }
+ );
+ ```
+
+
+
+Pinning is **idempotent**, and a message has a single pinner: re-pinning an already pinned message updates `pinnedBy` and `pinnedAt` to the most recent pinner rather than failing.
+
+## Unpin a Message
+
+
+
+ ```typescript
+ CometChat.unpinMessage(100).then(
+ (message: CometChat.BaseMessage) => {
+ console.log("Message unpinned:", message);
+ },
+ (error: CometChat.CometChatException) => {
+ console.log("Failed to unpin message:", error);
+ }
+ );
+ ```
+
+
+ ```javascript
+ CometChat.unpinMessage(100).then(
+ (message) => {
+ console.log("Message unpinned:", message);
+ },
+ (error) => {
+ console.log("Failed to unpin message:", error);
+ }
+ );
+ ```
+
+
+
+The resolved message comes back with its pin attributes cleared, so you can swap the rendered message straight into your list.
+
+## Fetch Pinned Messages
+
+Build a `MessagesRequest` with `setPinned(true)`. A pinned list belongs to one conversation, so pair it with `setUID()` for a one-on-one conversation or `setGUID()` for a group — exactly one of the two is required.
+
+
+
+ ```typescript
+ let messagesRequest: CometChat.MessagesRequest =
+ new CometChat.MessagesRequestBuilder()
+ .setUID("cometchat-uid-1")
+ .setPinned(true)
+ .setLimit(50)
+ .build();
+
+ messagesRequest.fetchPrevious().then(
+ (messages: CometChat.BaseMessage[]) => {
+ console.log("Pinned messages:", messages);
+ },
+ (error: CometChat.CometChatException) => {
+ console.log("Failed to fetch pinned messages:", error);
+ }
+ );
+ ```
+
+
+ ```javascript
+ let messagesRequest = new CometChat.MessagesRequestBuilder()
+ .setGUID("cometchat-guid-1")
+ .setPinned(true)
+ .setLimit(50)
+ .build();
+
+ messagesRequest.fetchPrevious().then(
+ (messages) => {
+ console.log("Pinned messages:", messages);
+ },
+ (error) => {
+ console.log("Failed to fetch pinned messages:", error);
+ }
+ );
+ ```
+
+
+
+The list is ordered by **pin time, most recently pinned first** — not by when the messages were sent. Render it in the order the SDK returns it.
+
+
+ A conversation can hold at most 100 pinned messages, so `setLimit(100)` fetches
+ the whole list in a single call — which is what you want when showing a pinned
+ count in a header.
+
+
+## Check if a Message is Pinned
+
+Every `BaseMessage` carries its pin state. The presence of `pinnedAt` *is* the boolean — an unpinned message simply has no pin attributes.
+
+
+
+ ```typescript
+ if (message.isPinned()) {
+ console.log("Pinned at:", message.getPinnedAt());
+ console.log("Pinned by:", message.getPinnedBy());
+
+ if (message.isSystemPinned()) {
+ // Pinned globally by an admin from the Dashboard,
+ // not by a member of the conversation
+ }
+ }
+ ```
+
+
+ ```javascript
+ if (message.isPinned()) {
+ console.log("Pinned at:", message.getPinnedAt());
+ console.log("Pinned by:", message.getPinnedBy());
+
+ if (message.isSystemPinned()) {
+ // Pinned globally by an admin from the Dashboard,
+ // not by a member of the conversation
+ }
+ }
+ ```
+
+
+
+| Method | Returns |
+| ------------------- | -------------------------------------------------------------------------- |
+| `isPinned()` | `true` when the message is pinned. |
+| `getPinnedAt()` | The pin timestamp, or `undefined`. |
+| `getPinnedBy()` | The UID of the most recent pinner, or `undefined`. |
+| `isSystemPinned()` | `true` for an admin/global pin. Render it as a system pin, not as a user. |
+
+## Real-time Pin Events
+
+Pin and unpin are broadcast to everyone in the conversation. Add the callbacks to your existing `MessageListener`.
+
+
+
+ ```typescript
+ CometChat.addMessageListener(
+ "UNIQUE_LISTENER_ID",
+ new CometChat.MessageListener({
+ onMessagePinned: (message: CometChat.BaseMessage) => {
+ console.log("Message pinned:", message);
+ },
+ onMessageUnpinned: (message: CometChat.BaseMessage) => {
+ console.log("Message unpinned:", message);
+ },
+ })
+ );
+ ```
+
+
+ ```javascript
+ CometChat.addMessageListener(
+ "UNIQUE_LISTENER_ID",
+ new CometChat.MessageListener({
+ onMessagePinned: (message) => {
+ console.log("Message pinned:", message);
+ },
+ onMessageUnpinned: (message) => {
+ console.log("Message unpinned:", message);
+ },
+ })
+ );
+ ```
+
+
+
+Each callback receives the **full updated message**, so you can replace the message in your list without a follow-up fetch.
+
+## Pin Limit
+
+A conversation holds a capped number of pins, configurable per app. Read the cap rather than hard-coding it — it is tenant-overridable and will drift.
+
+
+
+ ```typescript
+ let limit: number | null = await CometChat.getPinnedMessagesLimit();
+ let systemLimit: number | null =
+ await CometChat.getSystemPinnedMessagesLimit();
+
+ if (limit !== null && pinnedCount >= limit) {
+ // Disable the pin control instead of letting the user hit the error
+ }
+ ```
+
+
+ ```javascript
+ CometChat.getPinnedMessagesLimit().then((limit) => {
+ if (limit !== null && pinnedCount >= limit) {
+ // Disable the pin control instead of letting the user hit the error
+ }
+ });
+ ```
+
+
+
+Both resolve to `null` when the app settings carry no value — show generic copy in that case rather than guessing a number. `getSystemPinnedMessagesLimit()` is the separate cap for admin/global pins, enforced independently of the per-user one.
+
+## Feature Availability
+
+Check whether Pin Message is enabled for your app before showing pin actions.
+
+
+
+ ```typescript
+ let enabled: boolean = await CometChat.isPinMessageEnabled();
+
+ if (enabled) {
+ // Show the Pin option
+ }
+ ```
+
+
+ ```javascript
+ CometChat.isPinMessageEnabled().then((enabled) => {
+ if (enabled) {
+ // Show the Pin option
+ }
+ });
+ ```
+
+
+
+This resolves `false` rather than rejecting when the flag is missing or settings are unavailable, so an unavailable setting degrades to "hidden" instead of an unhandled rejection.
+
+---
+
+## Next Steps
+
+
+
+ Bookmark a message privately, across conversations
+
+
+ Pin a conversation to the top of the list
+
+
+ Every listener the SDK exposes, in one place
+
+
+ Filter messages by pinned, saved, type, tags and more
+
+
diff --git a/sdk/javascript/save-message.mdx b/sdk/javascript/save-message.mdx
new file mode 100644
index 000000000..2d8e32c94
--- /dev/null
+++ b/sdk/javascript/save-message.mdx
@@ -0,0 +1,302 @@
+---
+title: "Save A Message"
+description: "Save and unsave messages privately, fetch the saved list across conversations, and listen for save events with the CometChat JavaScript SDK."
+---
+
+Saving bookmarks a message for the logged-in user. Unlike a pin, a save is **private and cross-conversation**: nobody else can see it, no role is required, and the saved list spans every conversation the user is part of.
+
+
+ `savedAt` is per-viewer. It is only ever populated in the acting user's own
+ context — you will never see another user's saves on a message.
+
+
+## Save a Message
+
+Call `saveMessage()` with the message's ID. It resolves with the full updated message, with `savedAt` set.
+
+
+
+ ```typescript
+ let messageId: number = 100;
+
+ CometChat.saveMessage(messageId).then(
+ (message: CometChat.BaseMessage) => {
+ console.log("Message saved:", message);
+ },
+ (error: CometChat.CometChatException) => {
+ console.log("Failed to save message:", error);
+ }
+ );
+ ```
+
+
+ ```javascript
+ let messageId = 100;
+
+ CometChat.saveMessage(messageId).then(
+ (message) => {
+ console.log("Message saved:", message);
+ },
+ (error) => {
+ console.log("Failed to save message:", error);
+ }
+ );
+ ```
+
+
+
+Saving is idempotent — saving an already saved message succeeds rather than failing.
+
+## Unsave a Message
+
+
+
+ ```typescript
+ CometChat.unsaveMessage(100).then(
+ (message: CometChat.BaseMessage) => {
+ console.log("Message unsaved:", message);
+ },
+ (error: CometChat.CometChatException) => {
+ console.log("Failed to unsave message:", error);
+ }
+ );
+ ```
+
+
+ ```javascript
+ CometChat.unsaveMessage(100).then(
+ (message) => {
+ console.log("Message unsaved:", message);
+ },
+ (error) => {
+ console.log("Failed to unsave message:", error);
+ }
+ );
+ ```
+
+
+
+The resolved message comes back with `savedAt` cleared, never left stale.
+
+## Fetch Saved Messages
+
+Build a `MessagesRequest` with `setSaved(true)`. The saved list is **user-level**, so unlike the pinned list you do **not** set a UID or a GUID — leaving both unset is what makes it cross-conversation.
+
+
+
+ ```typescript
+ let messagesRequest: CometChat.MessagesRequest =
+ new CometChat.MessagesRequestBuilder()
+ .setSaved(true)
+ .setLimit(50)
+ .build();
+
+ messagesRequest.fetchPrevious().then(
+ (messages: CometChat.BaseMessage[]) => {
+ console.log("Saved messages:", messages);
+ },
+ (error: CometChat.CometChatException) => {
+ console.log("Failed to fetch saved messages:", error);
+ }
+ );
+ ```
+
+
+ ```javascript
+ let messagesRequest = new CometChat.MessagesRequestBuilder()
+ .setSaved(true)
+ .setLimit(50)
+ .build();
+
+ messagesRequest.fetchPrevious().then(
+ (messages) => {
+ console.log("Saved messages:", messages);
+ },
+ (error) => {
+ console.log("Failed to fetch saved messages:", error);
+ }
+ );
+ ```
+
+
+
+The list comes back newest message first. Call `fetchPrevious()` again on the same object to page through older entries.
+
+Because the list spans conversations, every row carries its own context — use `getConversationId()`, `getReceiverId()` and `getReceiverType()` to route a tap on a saved message back to the right conversation.
+
+
+
+ ```typescript
+ messages.forEach((message: CometChat.BaseMessage) => {
+ console.log(
+ message.getConversationId(),
+ message.getReceiverType(), // "user" or "group"
+ message.getReceiverId()
+ );
+ });
+ ```
+
+
+ ```javascript
+ messages.forEach((message) => {
+ console.log(
+ message.getConversationId(),
+ message.getReceiverType(), // "user" or "group"
+ message.getReceiverId()
+ );
+ });
+ ```
+
+
+
+## Check if a Message is Saved
+
+The presence of `savedAt` *is* the boolean — an unsaved message simply has no save attribute.
+
+
+
+ ```typescript
+ if (message.isSaved()) {
+ console.log("Saved at:", message.getSavedAt());
+ }
+ ```
+
+
+ ```javascript
+ if (message.isSaved()) {
+ console.log("Saved at:", message.getSavedAt());
+ }
+ ```
+
+
+
+| Method | Returns |
+| ---------------- | ------------------------------------------ |
+| `isSaved()` | `true` when the logged-in user saved it. |
+| `getSavedAt()` | The save timestamp, or `undefined`. |
+
+## Real-time Save Events
+
+Save and unsave are **private multi-device** events: they are delivered to the user's other logged-in sessions so a save on the phone shows up on the desktop. Add the callbacks to your existing `MessageListener`.
+
+
+
+ ```typescript
+ CometChat.addMessageListener(
+ "UNIQUE_LISTENER_ID",
+ new CometChat.MessageListener({
+ onMessageSaved: (message: CometChat.BaseMessage) => {
+ console.log("Message saved:", message);
+ },
+ onMessageUnsaved: (message: CometChat.BaseMessage) => {
+ console.log("Message unsaved:", message);
+ },
+ })
+ );
+ ```
+
+
+ ```javascript
+ CometChat.addMessageListener(
+ "UNIQUE_LISTENER_ID",
+ new CometChat.MessageListener({
+ onMessageSaved: (message) => {
+ console.log("Message saved:", message);
+ },
+ onMessageUnsaved: (message) => {
+ console.log("Message unsaved:", message);
+ },
+ })
+ );
+ ```
+
+
+
+
+ The device that performed the save also receives its own event exactly once —
+ the SDK de-duplicates the write's own echo, so you can insert into your saved
+ list from the callback alone without double-counting.
+
+
+## Save Limit
+
+A user may save a capped number of messages across all conversations. Read the cap from app settings rather than hard-coding it.
+
+
+
+ ```typescript
+ let limit: number | null = await CometChat.getSavedMessagesLimit();
+
+ if (limit !== null && savedCount >= limit) {
+ // Disable the save control instead of letting the user hit the error
+ }
+ ```
+
+
+ ```javascript
+ CometChat.getSavedMessagesLimit().then((limit) => {
+ if (limit !== null && savedCount >= limit) {
+ // Disable the save control instead of letting the user hit the error
+ }
+ });
+ ```
+
+
+
+It resolves to `null` when the app settings carry no value — show generic copy in that case rather than guessing a number.
+
+## Feature Availability
+
+
+
+ ```typescript
+ let enabled: boolean = await CometChat.isSaveMessageEnabled();
+
+ if (enabled) {
+ // Show the Save option
+ }
+ ```
+
+
+ ```javascript
+ CometChat.isSaveMessageEnabled().then((enabled) => {
+ if (enabled) {
+ // Show the Save option
+ }
+ });
+ ```
+
+
+
+This resolves `false` rather than rejecting when the flag is missing or settings are unavailable.
+
+---
+
+## Next Steps
+
+
+
+ Highlight a message for everyone in a conversation
+
+
+ Pin a conversation to the top of the list
+
+
+ Fetch and order the conversation list
+
+
+ Filter messages by saved, pinned, type, tags and more
+
+
diff --git a/sdk/javascript/thread-subscription.mdx b/sdk/javascript/thread-subscription.mdx
new file mode 100644
index 000000000..9189519c1
--- /dev/null
+++ b/sdk/javascript/thread-subscription.mdx
@@ -0,0 +1,349 @@
+---
+title: "Thread Subscription"
+description: "Subscribe and unsubscribe from message threads, read subscription state off a message, and fetch participated threads with the CometChat JavaScript SDK."
+---
+
+Thread subscription gives users control over thread noise. A user can **subscribe** to a message thread to be notified of future replies, or **unsubscribe** 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.
+
+
+ Thread subscription builds on [Threaded
+ Messages](/sdk/javascript/threaded-messages). A thread is identified by the ID
+ of its **parent message** — there is no separate thread ID.
+
+
+## How state works
+
+The SDK keeps **no subscription state of its own**. There is no cache and no listener to reconcile:
+
+- Every message fetch asks the server for the flag, and it arrives on the message — read it with `message.isThreadSubscribed()`.
+- `subscribeToThread()` and `unsubscribeFromThread()` resolve when the server has accepted the change. The resolved promise **is** the acknowledgement.
+
+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
+
+Use `subscribeToThread()` with the ID of the thread's parent message. The call is **idempotent** — subscribing to a thread the user already follows succeeds silently. Subscribing to a message with zero replies is allowed; the user is notified when the first reply arrives.
+
+
+
+ ```typescript
+ let parentMessageId: number = 100;
+
+ CometChat.subscribeToThread(parentMessageId).then(
+ (response: string) => {
+ // The server has accepted it — flip your toggle here.
+ console.log("Subscribed to thread:", response);
+ },
+ (error: CometChat.CometChatException) => {
+ console.log("Failed to subscribe:", error);
+ }
+ );
+ ```
+
+
+ ```javascript
+ let parentMessageId = 100;
+
+ CometChat.subscribeToThread(parentMessageId).then(
+ (response) => {
+ // The server has accepted it — flip your toggle here.
+ console.log("Subscribed to thread:", response);
+ },
+ (error) => {
+ console.log("Failed to subscribe:", error);
+ }
+ );
+ ```
+
+
+
+## Unsubscribe from a Thread
+
+Use `unsubscribeFromThread()`. This is idempotent too — unsubscribing from a thread the user does not follow succeeds silently.
+
+
+
+ ```typescript
+ CometChat.unsubscribeFromThread(100).then(
+ (response: string) => {
+ console.log("Unsubscribed from thread:", response);
+ },
+ (error: CometChat.CometChatException) => {
+ console.log("Failed to unsubscribe:", error);
+ }
+ );
+ ```
+
+
+ ```javascript
+ CometChat.unsubscribeFromThread(100).then(
+ (response) => {
+ console.log("Unsubscribed from thread:", response);
+ },
+ (error) => {
+ console.log("Failed to unsubscribe:", error);
+ }
+ );
+ ```
+
+
+
+
+ Unsubscribing is **not sticky**. If the user replies in the thread again, or is
+ @-mentioned in it, the server re-subscribes them. Do not promise users that they
+ will never hear about the thread again.
+
+
+Unsubscribing hard-deletes the subscription server-side, so a thread you are showing in a "following" list should be removed from that list when the call resolves.
+
+## Read the Subscription State
+
+The state rides the **parent message**. Read it with `isThreadSubscribed()`:
+
+
+
+ ```typescript
+ if (parentMessage.isThreadSubscribed()) {
+ // Show the "Unfollow" affordance
+ } else {
+ // Show the "Follow" affordance
+ }
+ ```
+
+
+ ```javascript
+ if (parentMessage.isThreadSubscribed()) {
+ // Show the "Unfollow" affordance
+ } else {
+ // Show the "Follow" affordance
+ }
+ ```
+
+
+
+Every message fetch the SDK makes asks the server for this flag, so any message you obtained from `MessagesRequest` or `getMessageDetails()` carries it.
+
+
+ A message delivered over the **socket** 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.
+
+
+### 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 `threadSubscribed: true`, 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 header 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:
+
+
+
+ ```typescript
+ await CometChat.subscribeToThread(100);
+
+ // The server accepted it — bring the objects you are rendering into line.
+ parentMessage.setThreadSubscribed(true);
+ ```
+
+
+ ```javascript
+ CometChat.subscribeToThread(100).then(() => {
+ // The server accepted it — bring the objects you are rendering into line.
+ parentMessage.setThreadSubscribed(true);
+ });
+ ```
+
+
+
+
+ `setThreadSubscribed()` is **local only** — it changes the object in memory and
+ sends nothing to the server. Use `subscribeToThread()` /
+ `unsubscribeFromThread()` to change the actual subscription.
+
+
+## Reacting to Replies
+
+A thread reply is an **ordinary message** with `parentMessageId` set, delivered through the standard `MessageListener` like any other message. There is no separate thread listener.
+
+
+
+ ```typescript
+ CometChat.addMessageListener(
+ "UNIQUE_LISTENER_ID",
+ new CometChat.MessageListener({
+ onTextMessageReceived: (message: CometChat.TextMessage) => {
+ const parentMessageId = message.getParentMessageId();
+ if (parentMessageId) {
+ // A reply landed in a thread — bump your thread row here.
+ }
+ },
+ })
+ );
+ ```
+
+
+ ```javascript
+ CometChat.addMessageListener(
+ "UNIQUE_LISTENER_ID",
+ new CometChat.MessageListener({
+ onTextMessageReceived: (message) => {
+ const parentMessageId = message.getParentMessageId();
+ if (parentMessageId) {
+ // A reply landed in a thread — bump your thread row here.
+ }
+ },
+ })
+ );
+ ```
+
+
+
+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()` promise instead.
+
+## Fetch the Threads a User Participates In
+
+Use `ThreadsRequest` to build a thread inbox. Every returned thread is one the logged-in user is subscribed to.
+
+
+
+ ```typescript
+ let threadsRequest: CometChat.ThreadsRequest =
+ new CometChat.ThreadsRequestBuilder()
+ .setParticipatedByMe(true)
+ .setLimit(30)
+ .build();
+
+ threadsRequest.fetchNext().then(
+ (threads: CometChat.MessageThread[]) => {
+ console.log("Threads fetched:", threads);
+ },
+ (error: CometChat.CometChatException) => {
+ console.log("Failed to fetch threads:", error);
+ }
+ );
+ ```
+
+
+ ```javascript
+ let threadsRequest = new CometChat.ThreadsRequestBuilder()
+ .setParticipatedByMe(true)
+ .setLimit(30)
+ .build();
+
+ threadsRequest.fetchNext().then(
+ (threads) => {
+ console.log("Threads fetched:", threads);
+ },
+ (error) => {
+ console.log("Failed to fetch threads:", error);
+ }
+ );
+ ```
+
+
+
+Call `fetchNext()` repeatedly on the same object to page through the list. Scope the list to one conversation with `setUid()` or `setGuid()` — the two are mutually exclusive.
+
+| Method | Description |
+| --------------------------- | ------------------------------------------------------------------- |
+| `setLimit(number)` | Threads per page. Accepts 1–1000. |
+| `setParticipatedByMe(bool)` | Restrict the list to threads the logged-in user participates in. |
+| `setUid(string)` | Only threads in the one-on-one conversation with this user. |
+| `setGuid(string)` | Only threads in this group. |
+
+### The MessageThread Model
+
+Each row is a `MessageThread`:
+
+| Method | Returns |
+| ------------------------- | ----------------------------------------------------------------- |
+| `getParentMessageId()` | The thread's identifier — the parent message's ID. |
+| `getParentMessage()` | The parent `BaseMessage`, or `null` if it could not be parsed. |
+| `getReplyCount()` | Number of replies in the thread. |
+| `getLastReply()` | The most recent reply, or `null`. |
+| `getUnreadReplyCount()` | Unread replies, or `null` when the server did not send a count. |
+| `isSubscribed()` | Whether the user follows this thread. Always `true` for list rows. |
+| `getUpdatedAt()` | When the thread last changed. |
+| `getConversationId()` | The conversation the thread belongs to. |
+| `getReceiverType()` | `"user"` or `"group"`. |
+| `getReceiverId()` | The peer's UID or the group's GUID. |
+
+
+ Sort a thread inbox on `getLastReply()?.getSentAt()` falling back to the parent
+ message's `sentAt` — a thread with no replies has no last reply.
+
+
+## Error Handling
+
+Both `subscribeToThread()` and `unsubscribeFromThread()` reject with a `CometChatException`. The most common client-side failure is an invalid parent message ID.
+
+
+
+ ```typescript
+ CometChat.subscribeToThread(0).then(
+ (response: string) => {
+ console.log("Subscribed:", response);
+ },
+ (error: CometChat.CometChatException) => {
+ // code: "INVALID_PARENT_MESSAGE_ID"
+ console.log(error.code, error.message);
+ }
+ );
+ ```
+
+
+ ```javascript
+ CometChat.subscribeToThread(0).then(
+ (response) => {
+ console.log("Subscribed:", response);
+ },
+ (error) => {
+ // code: "INVALID_PARENT_MESSAGE_ID"
+ console.log(error.code, error.message);
+ }
+ );
+ ```
+
+
+
+---
+
+## Next Steps
+
+
+
+ Send, receive and fetch messages inside a thread
+
+
+ Every listener the SDK exposes, in one place
+
+
+ Mention users in messages and filter for your own mentions
+
+
+ Filter messages by thread, type, tags and more
+
+
diff --git a/sdk/javascript/threaded-messages.mdx b/sdk/javascript/threaded-messages.mdx
index 1d6af168f..c9dd0491b 100644
--- a/sdk/javascript/threaded-messages.mdx
+++ b/sdk/javascript/threaded-messages.mdx
@@ -168,6 +168,75 @@ messagesRequest.fetchPrevious().then(
The `fetchPrevious()` method returns an array of [`BaseMessage`](/sdk/reference/messages#basemessage) objects representing thread replies.
+### Hydrate the Parent Message
+
+A thread reply carries its `parentMessageId`, but not the parent message itself. Use `withParent(true)` when you need the parent hydrated alongside each reply — for example, to render a saved or pinned reply with the message it answers.
+
+
+
+```typescript
+let messagesRequest: CometChat.MessagesRequest =
+ new CometChat.MessagesRequestBuilder()
+ .setLimit(30)
+ .setParentMessageId(100)
+ .withParent(true)
+ .build();
+```
+
+
+
+
+```javascript
+let messagesRequest = new CometChat.MessagesRequestBuilder()
+ .setLimit(30)
+ .setParentMessageId(100)
+ .withParent(true)
+ .build();
+```
+
+
+
+
+
+`withParent(true)` works on any message fetch, including the pinned and saved lists.
+
+## Subscribe to a Thread
+
+Users can follow a thread to be notified of future replies, or unfollow it to mute the noise. The server subscribes them automatically when they start a thread, reply in it, or are @-mentioned in it, and they can subscribe explicitly to any parent message. Read the current state off the parent message with `isThreadSubscribed()`.
+
+
+
+```typescript
+CometChat.subscribeToThread(100).then(
+ (response: string) => {
+ console.log("Subscribed to thread:", response);
+ },
+ (error: CometChat.CometChatException) => {
+ console.log("Failed to subscribe:", error);
+ }
+);
+```
+
+
+
+
+```javascript
+CometChat.subscribeToThread(100).then(
+ (response) => {
+ console.log("Subscribed to thread:", response);
+ },
+ (error) => {
+ console.log("Failed to subscribe:", error);
+ }
+);
+```
+
+
+
+
+
+See [Thread Subscription](/sdk/javascript/thread-subscription) for unsubscribing, reading the subscription state, real-time thread events and fetching the threads a user participates in.
+
## Avoid Threaded Messages in User/Group Conversations
Use `hideReplies(true)` to exclude threaded messages when fetching messages for a conversation.