From e9639626a6abd87aa23563bf9648b6c8f7e0c844 Mon Sep 17 00:00:00 2001 From: PrajwalDhuleCC Date: Sun, 16 Aug 2026 23:47:22 +0530 Subject: [PATCH] feat(pin-save-thread-subscription-js-sdk): add documentation for pin/save messages, pin conversations and thread subscription --- docs.json | 4 + sdk/javascript/all-real-time-listeners.mdx | 84 ++++- sdk/javascript/message-filtering.mdx | 95 ++++++ sdk/javascript/pin-conversation.mdx | 284 +++++++++++++++++ sdk/javascript/pin-message.mdx | 298 ++++++++++++++++++ sdk/javascript/save-message.mdx | 302 ++++++++++++++++++ sdk/javascript/thread-subscription.mdx | 349 +++++++++++++++++++++ sdk/javascript/threaded-messages.mdx | 69 ++++ 8 files changed, 1481 insertions(+), 4 deletions(-) create mode 100644 sdk/javascript/pin-conversation.mdx create mode 100644 sdk/javascript/pin-message.mdx create mode 100644 sdk/javascript/save-message.mdx create mode 100644 sdk/javascript/thread-subscription.mdx 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.