From 63dacd7848165f0f4b6b46479888b68e4e9be81c Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Mon, 7 Sep 2026 17:53:48 +0530 Subject: [PATCH 1/6] feat(pin-save-thread-subscription-react-native-sdk): add documentation for pin/save messages, pin conversations and thread subscription MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors cometchat/docs#469 (JavaScript SDK) for the React Native SDK. New pages: - sdk/react-native/pin-message - sdk/react-native/save-message - sdk/react-native/pin-conversation - sdk/react-native/thread-subscription Updated: - additional-message-filtering: setPinnedOnly / setSavedOnly sections - real-time-listeners: onMessagePinned/Unpinned/Saved/Unsaved rows - threaded-messages: withParent() and a Subscribe to a Thread section - docs.json: nav entries, mirroring the JavaScript placement Written against the React Native SDK surface rather than translated from the JavaScript pages, because the two APIs diverge: - setPinnedOnly()/setSavedOnly(), not setPinned()/setSaved() - getPinMessageLimit(), getSystemPinMessageLimit(), getSaveMessageLimit(), getPinConversationLimit(), getSystemPinConversationLimit() — all spelled differently from their JavaScript counterparts - no isPinned()/isSaved()/isSystemPinned() helpers; state is derived from getPinnedAt()/getSavedAt()/getPinnedBy() - no CometChat.PINNED_BY constant; setPinnedBy() takes the bare strings "me"/"system", matched case-sensitively and dropped silently when unmatched - no ConversationListener at all, so conversation pins have no real-time channel and the pages say to re-fetch instead Also documents React Native behaviour absent from the JavaScript pages: the 5-per-user conversation pin cap, the unseeded features.ux.conversations.pinned.enabled flag, errorParams.limit on cap breach, the ThreadsRequestBuilder setUid/setGuid casing, and getUnreadReplyCount() returning null for unknown rather than zero. Co-Authored-By: Claude Opus 5 (1M context) --- docs.json | 4 + .../additional-message-filtering.mdx | 95 ++++ sdk/react-native/pin-conversation.mdx | 395 +++++++++++++++++ sdk/react-native/pin-message.mdx | 408 ++++++++++++++++++ sdk/react-native/real-time-listeners.mdx | 22 + sdk/react-native/save-message.mdx | 364 ++++++++++++++++ sdk/react-native/thread-subscription.mdx | 347 +++++++++++++++ sdk/react-native/threaded-messages.mdx | 69 +++ 8 files changed, 1704 insertions(+) create mode 100644 sdk/react-native/pin-conversation.mdx create mode 100644 sdk/react-native/pin-message.mdx create mode 100644 sdk/react-native/save-message.mdx create mode 100644 sdk/react-native/thread-subscription.mdx diff --git a/docs.json b/docs.json index f17ba2a8b..b631d1fa2 100644 --- a/docs.json +++ b/docs.json @@ -3540,10 +3540,14 @@ "sdk/react-native/additional-message-filtering", "sdk/react-native/retrieve-conversations", "sdk/react-native/threaded-messages", + "sdk/react-native/thread-subscription", "sdk/react-native/edit-message", "sdk/react-native/delete-message", "sdk/react-native/flag-message", + "sdk/react-native/pin-message", + "sdk/react-native/save-message", "sdk/react-native/delete-conversation", + "sdk/react-native/pin-conversation", "sdk/react-native/typing-indicators", "sdk/react-native/transient-messages", "sdk/react-native/delivery-read-receipts", diff --git a/sdk/react-native/additional-message-filtering.mdx b/sdk/react-native/additional-message-filtering.mdx index 74ce76e6e..19a60f89d 100644 --- a/sdk/react-native/additional-message-filtering.mdx +++ b/sdk/react-native/additional-message-filtering.mdx @@ -1473,6 +1473,101 @@ let messagesRequest = new CometChat.MessagesRequestBuilder() The response contains media message objects filtered to the specified attachment types. +## Pinned messages + +Use `setPinnedOnly(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) + .setPinnedOnly(true) + .build(); +``` + + + + +```javascript +let UID = "UID"; +let limit = 50; +let messagesRequest = new CometChat.MessagesRequestBuilder() + .setUID(UID) + .setLimit(limit) + .setPinnedOnly(true) + .build(); +``` + + + + +```typescript +let GUID: string = "GUID", + limit: number = 50, + messagesRequest: CometChat.MessagesRequest = + new CometChat.MessagesRequestBuilder() + .setGUID(GUID) + .setLimit(limit) + .setPinnedOnly(true) + .build(); +``` + + + + +```javascript +let GUID = "GUID"; +let limit = 50; +let messagesRequest = new CometChat.MessagesRequestBuilder() + .setGUID(GUID) + .setLimit(limit) + .setPinnedOnly(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/react-native/pin-message) for the full feature. + +## Saved messages + +Use `setSavedOnly(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) + .setSavedOnly(true) + .build(); +``` + + + + +```javascript +let limit = 50; +let messagesRequest = new CometChat.MessagesRequestBuilder() + .setLimit(limit) + .setSavedOnly(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/react-native/save-message) for the full feature. + ## Next Steps diff --git a/sdk/react-native/pin-conversation.mdx b/sdk/react-native/pin-conversation.mdx new file mode 100644 index 000000000..f5dcbcd1d --- /dev/null +++ b/sdk/react-native/pin-conversation.mdx @@ -0,0 +1,395 @@ +--- +title: "Pin A Conversation" +sidebarTitle: "Pin A Conversation" +description: "Pin and unpin conversations and fetch the pinned conversation list with the CometChat React Native SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```javascript +// Pin / unpin — addressed by peer + type, not by conversationId +await CometChat.pinConversation("UID", CometChat.RECEIVER_TYPE.USER); +await CometChat.unpinConversation("GUID", CometChat.RECEIVER_TYPE.GROUP); + +// The DEFAULT list is already pin-ordered — no filter needed. +// To narrow to pins only, pass the raw string tokens: +const request = new CometChat.ConversationsRequestBuilder() + .setPinnedBy(["system", "me"]) + .setLimit(30) + .build(); + +// Read pin state — there is no isPinned() +const isPinned = conversation.getPinnedAt() !== undefined; +const isSystemPinned = conversation.getPinnedBy() === "app_system"; + +// Cap and availability (off by default — must be mapped per app) +const limit = await CometChat.getPinConversationLimit(); +const enabled = await CometChat.isPinConversationEnabled(); +``` + + + +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. + + + Pin Conversation requires the `features.ux.conversations.pinned.enabled` flag, + which is **not seeded in any plan** and must be mapped per app. Until it is, + every call rejects with `ERR_FEATURE_NOT_ACCESSIBLE`. Gate your UI on + `isPinConversationEnabled()` before showing the control. + + + + 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-uid-1", + CometChat.RECEIVER_TYPE.USER +).then( + (conversation) => { + console.log("Conversation pinned:", conversation); + }, + (error) => { + console.log("Failed to pin conversation:", error); + } +); +``` + + + + +```typescript +CometChat.pinConversation( + "cometchat-guid-1", + CometChat.RECEIVER_TYPE.GROUP +).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. + + + A conversation that has **never been messaged**, or that is hidden, cannot be + newly pinned — the call rejects with `ERR_CONVERSATION_NOT_ACCESSIBLE`. Pin + from a conversation that already exists in the list. + + +## 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); + } +); +``` + + + + + +Unpinning returns the conversation to its recency position. A user **cannot** unpin an admin-global pin — hide or disable the unpin control for conversations where `getPinnedBy()` is `"app_system"`. + +## 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. Each row already carries `pinnedAt` and `pinnedBy`, so a pinned strip usually needs **no separate call**. + +When you do want the pinned set standalone, use `setPinnedBy()`. + + + +```typescript +let conversationsRequest: CometChat.ConversationsRequest = + new CometChat.ConversationsRequestBuilder() + .setPinnedBy(["system", "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(["me"]) + .setLimit(30) + .build(); + +conversationsRequest.fetchNext().then( + (conversations) => { + console.log("Pinned conversations:", conversations); + }, + (error) => { + console.log("Failed to fetch conversations:", error); + } +); +``` + + + + + +| Token | Meaning | +| ------------ | ---------------------------------------------------- | +| `"me"` | Conversations the logged-in user pinned themselves. | +| `"system"` | Conversations pinned globally by an admin. | + + + The match is **exact and case-sensitive**, and an unrecognised value is dropped + silently rather than raising. `setPinnedBy(["Me"])` leaves nothing behind, the + `pinnedBy` parameter is omitted entirely, and you get the **full conversation + list** back — not an error, and not a pinned-only list. Passing an empty array + does the same thing. Use the lowercase tokens exactly as written above. + + + + These are plain strings in the React Native SDK — there is no + `CometChat.PINNED_BY` constant to import. The server rejects any other value + with `ERR_BAD_REQUEST` — end users cannot query another user's pins — so the + builder drops unrecognised entries before the request is sent. + + +## Check if a Conversation is Pinned + +As with messages, the React Native SDK has **no `isPinned()` helper** — the presence of `pinnedAt` *is* the boolean. + + + +```typescript +const isPinned: boolean = conversation.getPinnedAt() !== undefined; + +if (isPinned) { + console.log("Pinned at:", conversation.getPinnedAt()); + console.log("Pinned by:", conversation.getPinnedBy()); + + // A user may not unpin an admin/global pin + const isSystemPinned: boolean = conversation.getPinnedBy() === "app_system"; +} +``` + + + + +```javascript +const isPinned = conversation.getPinnedAt() !== undefined; + +if (isPinned) { + console.log("Pinned at:", conversation.getPinnedAt()); + console.log("Pinned by:", conversation.getPinnedBy()); + + // A user may not unpin an admin/global pin + const isSystemPinned = conversation.getPinnedBy() === "app_system"; +} +``` + + + + + +| Method | Returns | +| ----------------- | ------------------------------------------------------------------------------- | +| `getPinnedAt()` | The pin timestamp, or `undefined` when the conversation is not pinned. | +| `getPinnedBy()` | The pinner's UID, or `"app_system"` for an admin/global pin, or `undefined`. | + +## Keeping the List in Sync + + + The React Native SDK does **not** expose a `ConversationListener`, and there + are no `onConversationPinned` / `onConversationUnpinned` callbacks. Conversation + pins made on another device do not arrive over the socket. + + +Update your list from the promise on the device that performed the action, and re-fetch the conversation list when the screen regains focus to pick up pins made elsewhere. + + + +```typescript +const conversation: CometChat.Conversation = await CometChat.pinConversation( + "cometchat-uid-1", + CometChat.RECEIVER_TYPE.USER +); + +// The server accepted it — move the row yourself. +setConversations((prev) => reorderWithPinnedFirst(prev, conversation)); +``` + + + + +```javascript +CometChat.pinConversation("cometchat-uid-1", CometChat.RECEIVER_TYPE.USER).then( + (conversation) => { + // The server accepted it — move the row yourself. + setConversations((prev) => reorderWithPinnedFirst(prev, conversation)); + } +); +``` + + + + + +## Pin Limit + + + +```typescript +let limit: number | null = await CometChat.getPinConversationLimit(); +let systemLimit: number | null = await CometChat.getSystemPinConversationLimit(); +``` + + + + +```javascript +CometChat.getPinConversationLimit().then((limit) => { + console.log("Pinned conversations limit:", limit); +}); +``` + + + + + +Both resolve to `null` when the app settings carry no value. `getSystemPinConversationLimit()` is the separate admin/global cap, enforced independently — system pins do not consume a user's allowance. + + + The conversation pin cap is **much smaller than the message pin cap** — 5 per + user by default, not 100. Read it rather than assuming. + + +## 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 + } +}); +``` + + + + + +Unlike the two message-level flags, this one is **not seeded anywhere and is off by default** — treat a `false` as the normal case for a new app, not as an error. + +--- + +## Next Steps + + + + Fetch and order the conversation list + + + Highlight a message for everyone in a conversation + + + Bookmark a message privately, across conversations + + + Remove a conversation from the list + + diff --git a/sdk/react-native/pin-message.mdx b/sdk/react-native/pin-message.mdx new file mode 100644 index 000000000..f6bcbd882 --- /dev/null +++ b/sdk/react-native/pin-message.mdx @@ -0,0 +1,408 @@ +--- +title: "Pin A Message" +sidebarTitle: "Pin A Message" +description: "Pin and unpin messages in a conversation, fetch the pinned list, and listen for pin events with the CometChat React Native SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```javascript +// Pin / unpin a message +const pinned = await CometChat.pinMessage(messageId); +const unpinned = await CometChat.unpinMessage(messageId); + +// Fetch the pinned list for one conversation +const request = new CometChat.MessagesRequestBuilder() + .setUID("UID") // or .setGUID("GUID") + .setPinnedOnly(true) + .setLimit(50) + .build(); +const messages = await request.fetchPrevious(); + +// Read pin state off a message — there is no isPinned() +const isPinned = message.getPinnedAt() !== undefined; + +// Caps and availability +const limit = await CometChat.getPinMessageLimit(); // number | null +const enabled = await CometChat.isPinMessageEnabled(); // boolean +``` + + + +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, Moderator or group Owner 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: string = "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. + + + On a cap breach the rejection carries the server-owned ceiling in + `errorParams.limit`, so you can tell the user the actual number without a + second call. + + +## 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); + } +); +``` + + + + + +Any Admin, Moderator or Owner may unpin — not only whoever pinned it. 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 `setPinnedOnly(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") + .setPinnedOnly(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() + .setUID("cometchat-uid-1") + .setPinnedOnly(true) + .setLimit(50) + .build(); + +messagesRequest.fetchPrevious().then( + (messages) => { + console.log("Pinned messages:", messages); + }, + (error) => { + console.log("Failed to fetch pinned messages:", error); + } +); +``` + + + + +```typescript +let messagesRequest: CometChat.MessagesRequest = + new CometChat.MessagesRequestBuilder() + .setGUID("cometchat-guid-1") + .setPinnedOnly(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") + .setPinnedOnly(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. + +Page it like any other message list: `fetchPrevious()` for older rows, `fetchNext()` for newer, stopping when a call resolves `[]`. The one difference is invisible to you — the cursor rides on `pinnedAt` instead of `sentAt`, and the SDK swaps it for you. + +## Check if a Message is Pinned + +Every `BaseMessage` carries its pin state as attributes. The React Native SDK has **no `isPinned()` helper** — the presence of `pinnedAt` *is* the boolean, and an unpinned message simply has no pin attributes. + + + +```typescript +const isPinned: boolean = message.getPinnedAt() !== undefined; + +if (isPinned) { + console.log("Pinned at:", message.getPinnedAt()); + console.log("Pinned by:", message.getPinnedBy()); + + // An admin/global pin is stamped with the reserved "app_system" pinner + // rather than a real UID — render it as a system pin, not as a user. + const isSystemPinned: boolean = message.getPinnedBy() === "app_system"; +} +``` + + + + +```javascript +const isPinned = message.getPinnedAt() !== undefined; + +if (isPinned) { + console.log("Pinned at:", message.getPinnedAt()); + console.log("Pinned by:", message.getPinnedBy()); + + // An admin/global pin is stamped with the reserved "app_system" pinner + // rather than a real UID — render it as a system pin, not as a user. + const isSystemPinned = message.getPinnedBy() === "app_system"; +} +``` + + + + + +| Method | Returns | +| ----------------- | ------------------------------------------------------------------- | +| `getPinnedAt()` | The pin timestamp, or `undefined` when the message is not pinned. | +| `getPinnedBy()` | The UID of the most recent pinner, or `undefined`. | + + + Do not port `isPinned()` or `isSystemPinned()` from the JavaScript SDK — they + do not exist in the React Native SDK. Derive both from `getPinnedAt()` and + `getPinnedBy()` as shown above. + + +## 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 `getPinnedAt()` and `getPinnedBy()` are readable without a follow-up fetch — replace the message in your list directly. + +Remove the listener when the screen unmounts: + + + +```typescript +CometChat.removeMessageListener("UNIQUE_LISTENER_ID"); +``` + + + + +```javascript +CometChat.removeMessageListener("UNIQUE_LISTENER_ID"); +``` + + + + + +## 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.getPinMessageLimit(); +let systemLimit: number | null = await CometChat.getSystemPinMessageLimit(); + +if (limit !== null && pinnedCount >= limit) { + // Disable the pin control instead of letting the user hit the error +} +``` + + + + +```javascript +CometChat.getPinMessageLimit().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. `getSystemPinMessageLimit()` is the separate cap for admin/global pins: system pins do **not** consume a user's allowance, so the two budgets are enforced independently. + +## 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/react-native/real-time-listeners.mdx b/sdk/react-native/real-time-listeners.mdx index 1a8a385c1..7a8114ff3 100644 --- a/sdk/react-native/real-time-listeners.mdx +++ b/sdk/react-native/real-time-listeners.mdx @@ -352,6 +352,10 @@ Receive events for incoming messages, typing indicators, read/delivery receipts, | **onMessageReactionAdded(receipt: [CometChat.ReactionEvent](/sdk/reference/auxiliary#reactionevent))** | This event is triggered when a reaction is added to a message in a user/group conversation. | | **onMessageReactionRemoved(receipt: [CometChat.ReactionEvent](/sdk/reference/auxiliary#reactionevent))** | This event is triggered when a reaction is removed from a message in a user/group conversation. | | **onCardMessageReceived(message: CometChat.CardMessage)** | This event is triggered when a card message (`category: "card"`) is received. See [Campaigns → Card Messages](/sdk/react-native/campaigns#card-messages). | +| **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. | To add the `MessageListener`: @@ -491,6 +495,24 @@ CometChat.removeMessageListener(listenerID); + + Pin and unpin are **conversation-wide** — every participant receives them. + Save and unsave are **private multi-device**: they reach the saving user's + other sessions, not the device that performed the save, which already has the + message resolved from the `saveMessage()` promise. Each callback carries the + full updated message, so `getPinnedAt()` / `getSavedAt()` are readable without + a further fetch. See [Pin A Message](/sdk/react-native/pin-message) and + [Save A Message](/sdk/react-native/save-message). + + + + **Conversation pins have no listener.** The React Native SDK exposes no + `ConversationListener` and no `onConversationPinned` / + `onConversationUnpinned` callbacks — unlike the JavaScript SDK. Update your + list from the `pinConversation()` promise and re-fetch on focus. See + [Pin A Conversation](/sdk/react-native/pin-conversation). + + ## Call Listener Receive events for incoming and outgoing call state changes. diff --git a/sdk/react-native/save-message.mdx b/sdk/react-native/save-message.mdx new file mode 100644 index 000000000..eb13369e9 --- /dev/null +++ b/sdk/react-native/save-message.mdx @@ -0,0 +1,364 @@ +--- +title: "Save A Message" +sidebarTitle: "Save A Message" +description: "Save and unsave messages privately, fetch the saved list across conversations, and listen for save events with the CometChat React Native SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```javascript +// Save / unsave a message +const saved = await CometChat.saveMessage(messageId); +const unsaved = await CometChat.unsaveMessage(messageId); + +// Fetch the saved list — user-level, so NO setUID()/setGUID() +const request = new CometChat.MessagesRequestBuilder() + .setSavedOnly(true) + .setLimit(50) + .build(); +const messages = await request.fetchPrevious(); + +// Read save state off a message — there is no isSaved() +const isSaved = message.getSavedAt() !== undefined; + +// Cap and availability +const limit = await CometChat.getSaveMessageLimit(); // number | null +const enabled = await CometChat.isSaveMessageEnabled(); // boolean +``` + + + +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, so the same + message reads saved for one user and unsaved for everyone else. + + +## Save a Message + +Call `saveMessage()` with the message's ID. It resolves with the full updated message, with `savedAt` set. + + + +```typescript +let messageId: string = "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. There is no role gate. On a cap breach the rejection carries the server-owned ceiling in `errorParams.limit`. + +## 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 `setSavedOnly(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() + .setSavedOnly(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() + .setSavedOnly(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 save first. Call `fetchPrevious()` again on the same object to page through older entries — this filter changes what the list contains, not how it pages. + + + Do **not** combine `setSavedOnly(true)` with `setUID()` or `setGUID()`. Saves + are account-wide; adding a scope narrows the list to one conversation and + quietly hides the rest. + + +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 React Native SDK has **no `isSaved()` helper** — the presence of `savedAt` *is* the boolean, and an unsaved message simply has no save attribute. + + + +```typescript +const isSaved: boolean = message.getSavedAt() !== undefined; + +if (isSaved) { + console.log("Saved at:", message.getSavedAt()); +} +``` + + + + +```javascript +const isSaved = message.getSavedAt() !== undefined; + +if (isSaved) { + console.log("Saved at:", message.getSavedAt()); +} +``` + + + + + +| Method | Returns | +| --------------- | ------------------------------------------------------------------ | +| `getSavedAt()` | The save timestamp, or `undefined` when the user has not saved it. | + + + Do not port `isSaved()` from the JavaScript SDK — it does not exist in the + React Native SDK. Derive it from `getSavedAt()` as shown above. + + +## 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); + }, + }) +); +``` + + + + + + + These callbacks fire for saves made on your **other** devices, not the one that + performed the save — that device already has the message resolved from + `saveMessage()`. Update your saved list from the promise there, and from these + callbacks everywhere else. + + +## 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.getSaveMessageLimit(); + +if (limit !== null && savedCount >= limit) { + // Disable the save control instead of letting the user hit the error +} +``` + + + + +```javascript +CometChat.getSaveMessageLimit().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/react-native/thread-subscription.mdx b/sdk/react-native/thread-subscription.mdx new file mode 100644 index 000000000..f63f1ced7 --- /dev/null +++ b/sdk/react-native/thread-subscription.mdx @@ -0,0 +1,347 @@ +--- +title: "Thread Subscription" +sidebarTitle: "Thread Subscription" +description: "Subscribe and unsubscribe from message threads, read subscription state off a message, and fetch participated threads with the CometChat React Native SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```javascript +// Subscribe / unsubscribe — a thread is identified by its PARENT message id +await CometChat.subscribeToThread(100); +await CometChat.unsubscribeFromThread(100); + +// Read state off a fetched parent message +const following = parentMessage.isThreadSubscribed(); + +// Align local copies after the server accepts (LOCAL ONLY — sends nothing) +parentMessage.setThreadSubscribed(true); + +// Thread inbox +const request = new CometChat.ThreadsRequestBuilder() + .setParticipatedByMe(true) + .setLimit(30) + .build(); +const threads = await request.fetchNext(); // CometChat.MessageThread[] +``` + + + +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/react-native/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 **hard-deletes** the server row, so a thread inbox built with + `ThreadsRequest` must drop that row rather than mark it unfollowed. It is also + **not sticky**: replying in the thread, or being @-mentioned in it, + re-subscribes the user. + + +## Read the Subscription State + + + +```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); + } +); +``` + + + + + +Scope the inbox to a single conversation with `setUid()` or `setGuid()`. Call `fetchNext()` again on the same object to page, and check `hasMore()` before doing so. + + + `ThreadsRequestBuilder` spells these `setUid()` / `setGuid()` — **not** the + `setUID()` / `setGUID()` used by `MessagesRequestBuilder`. The two are + mutually exclusive; set at most one. + + +### Reading a thread row + +Each row is a `MessageThread` — enough to render an inbox without fetching the parent message separately. + +| Method | Returns | +| ------------------------- | ----------------------------------------------------------------------- | +| `getParentMessageId()` | The thread's ID — pass it to `subscribeToThread()`. | +| `getParentMessage()` | The parent message, hydrated. | +| `getLastReply()` | The most recent reply, for the preview line. | +| `getReplyCount()` | Total replies in the thread. | +| `getUnreadReplyCount()` | Replies the user has not read — the badge count. | +| `getUpdatedAt()` | When the thread last changed, for ordering the inbox. | +| `getConversationId()` | The conversation the thread lives in. | +| `getReceiverId()` / `getReceiverType()` | Where to route a tap on the row. | +| `isSubscribed()` | Always `true` for rows from a `setParticipatedByMe(true)` fetch — presence in the list *is* the subscription. | + + + `getUnreadReplyCount()` returns `null`, not `0`, when the count is unknown. + Treat `null` as "no badge" rather than as zero unread. + + +--- + +## Next Steps + + + + Send and fetch replies inside a thread + + + @-mentions, which auto-subscribe a user to a thread + + + Every listener the SDK exposes, in one place + + + Bookmark a message privately, across conversations + + diff --git a/sdk/react-native/threaded-messages.mdx b/sdk/react-native/threaded-messages.mdx index 9e58b2928..9352bbb81 100644 --- a/sdk/react-native/threaded-messages.mdx +++ b/sdk/react-native/threaded-messages.mdx @@ -190,6 +190,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/react-native/thread-subscription) for unsubscribing, reading the subscription state, 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. From 4dfdd2cf1de4357168dae6d8303fdab07342e185 Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Mon, 7 Sep 2026 18:05:33 +0530 Subject: [PATCH 2/6] fix(react-native-sdk-docs): correct message id type, drop unverified caps, document moderation window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings on this PR. Message IDs are documented as `number` in pin-message and save-message, matching thread-subscription, threaded-messages and the JavaScript pages. They were `string` because the signature reads `messageId: string | any`, which was a misread — that union collapses to `any` and carries no type information. The runtime validator is `isNaN()` and rejects with "Message Id must be a number", so the string form contradicted the SDK's own error text. The conversation pin note no longer states "5 per user, not 100". Both numbers were unverified: our measured settings record `conversations.pinned.limit=10` and `messages.pinned.limit=5`, so the sentence may have been inverted. It also contradicted pin-message, which tells readers never to hard-code a cap. The note now states only that the two quotas are separate and must be read at runtime. Both pin-message and save-message now document the moderation window: a message that is still `moderation.status: "pending"` is refused with 403 ERR_MESSAGE_NO_ACCESS, which is the same code the server returns for a real permission refusal, so the two cannot be distinguished from the error alone. The window runs from send rather than from the user's tap, so the guidance is to retry rather than disable the control. Moderation is per-app and does not reproduce where it is switched off. Co-Authored-By: Claude Opus 5 (1M context) --- sdk/react-native/pin-conversation.mdx | 5 +++-- sdk/react-native/pin-message.mdx | 24 ++++++++++++++++++++---- sdk/react-native/save-message.mdx | 24 ++++++++++++++++++++---- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/sdk/react-native/pin-conversation.mdx b/sdk/react-native/pin-conversation.mdx index f5dcbcd1d..90efc0d8c 100644 --- a/sdk/react-native/pin-conversation.mdx +++ b/sdk/react-native/pin-conversation.mdx @@ -342,8 +342,9 @@ CometChat.getPinConversationLimit().then((limit) => { Both resolve to `null` when the app settings carry no value. `getSystemPinConversationLimit()` is the separate admin/global cap, enforced independently — system pins do not consume a user's allowance. - The conversation pin cap is **much smaller than the message pin cap** — 5 per - user by default, not 100. Read it rather than assuming. + The conversation pin cap and the message pin cap are **separate quotas with + different values**, and both are server-owned and tenant-overridable. Read + each one at runtime rather than assuming a number or reusing the other. ## Feature Availability diff --git a/sdk/react-native/pin-message.mdx b/sdk/react-native/pin-message.mdx index f6bcbd882..bf5ff15fa 100644 --- a/sdk/react-native/pin-message.mdx +++ b/sdk/react-native/pin-message.mdx @@ -45,7 +45,7 @@ Call `pinMessage()` with the message's ID. It resolves with the full updated mes ```typescript -let messageId: string = "100"; +let messageId: number = 100; CometChat.pinMessage(messageId).then( (message: CometChat.BaseMessage) => { @@ -61,7 +61,7 @@ CometChat.pinMessage(messageId).then( ```javascript -let messageId = "100"; +let messageId = 100; CometChat.pinMessage(messageId).then( (message) => { @@ -79,6 +79,22 @@ CometChat.pinMessage(messageId).then( 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. + + **A just-sent message may not be pinnable or savable yet.** On an app with + moderation enabled, the server stamps a new message `moderation.status: + "pending"` and clears it to `"approved"` a moment later. While it is pending, + `pinMessage()` rejects with `403 ERR_MESSAGE_NO_ACCESS` — the **same code it returns + for a genuine permission refusal**, so you cannot tell the two apart from the + error alone. + + Measured on a moderated app, the window runs from **send**, not from the user's + tap, and clears within a few seconds. Do not disable the control on this error: + by the time someone opens a menu and taps, moderation has usually finished. + Prefer a retry or a transient "not ready yet" message over telling the user + they lack permission. Moderation is configured **per app**, so this never + reproduces on an app that has it switched off. + + On a cap breach the rejection carries the server-owned ceiling in `errorParams.limit`, so you can tell the user the actual number without a @@ -90,7 +106,7 @@ Pinning is **idempotent**, and a message has a single pinner: re-pinning an alre ```typescript -CometChat.unpinMessage("100").then( +CometChat.unpinMessage(100).then( (message: CometChat.BaseMessage) => { console.log("Message unpinned:", message); }, @@ -104,7 +120,7 @@ CometChat.unpinMessage("100").then( ```javascript -CometChat.unpinMessage("100").then( +CometChat.unpinMessage(100).then( (message) => { console.log("Message unpinned:", message); }, diff --git a/sdk/react-native/save-message.mdx b/sdk/react-native/save-message.mdx index eb13369e9..d0732fec5 100644 --- a/sdk/react-native/save-message.mdx +++ b/sdk/react-native/save-message.mdx @@ -44,7 +44,7 @@ Call `saveMessage()` with the message's ID. It resolves with the full updated me ```typescript -let messageId: string = "100"; +let messageId: number = 100; CometChat.saveMessage(messageId).then( (message: CometChat.BaseMessage) => { @@ -60,7 +60,7 @@ CometChat.saveMessage(messageId).then( ```javascript -let messageId = "100"; +let messageId = 100; CometChat.saveMessage(messageId).then( (message) => { @@ -78,12 +78,28 @@ CometChat.saveMessage(messageId).then( Saving is idempotent — saving an already saved message succeeds rather than failing. There is no role gate. On a cap breach the rejection carries the server-owned ceiling in `errorParams.limit`. + + **A just-sent message may not be pinnable or savable yet.** On an app with + moderation enabled, the server stamps a new message `moderation.status: + "pending"` and clears it to `"approved"` a moment later. While it is pending, + `saveMessage()` rejects with `403 ERR_MESSAGE_NO_ACCESS` — the **same code it returns + for a genuine permission refusal**, so you cannot tell the two apart from the + error alone. + + Measured on a moderated app, the window runs from **send**, not from the user's + tap, and clears within a few seconds. Do not disable the control on this error: + by the time someone opens a menu and taps, moderation has usually finished. + Prefer a retry or a transient "not ready yet" message over telling the user + they lack permission. Moderation is configured **per app**, so this never + reproduces on an app that has it switched off. + + ## Unsave a Message ```typescript -CometChat.unsaveMessage("100").then( +CometChat.unsaveMessage(100).then( (message: CometChat.BaseMessage) => { console.log("Message unsaved:", message); }, @@ -97,7 +113,7 @@ CometChat.unsaveMessage("100").then( ```javascript -CometChat.unsaveMessage("100").then( +CometChat.unsaveMessage(100).then( (message) => { console.log("Message unsaved:", message); }, From 34c209f0c020620eac9813e56b442123eba9ed85 Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Mon, 7 Sep 2026 18:43:00 +0530 Subject: [PATCH 3/6] feat(pin-save-thread-subscription-react-native-uikit): document the UI Kit surface for pin/save, pin conversation and thread subscription MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the UI Kit half of this PR, alongside the SDK pages already here. New component pages: - ui-kit/react-native/pinned-messages (CometChatPinnedMessages) - ui-kit/react-native/saved-messages (CometChatSavedMessages) Updated: - message-list: hidePinMessageOption, hideUnpinMessageOption, hideSaveMessageOption, hideUnsaveMessageOption and hideThreadSubscriptionOption, in the visibility block and as per-prop sections in alphabetical order, plus a section on how the gates compose - conversations: pinConversationOptionVisibility and a Pinning Conversations section - docs.json: nav entries in the Components group, after message-list The point the pages lead with is that a `false` default on a hide* prop does not mean the option is visible. Pin and Save are gated twice — the integrator's PinSaveConfig opt-in, which defaults to OFF, ANDed with a server flag the kit resolves at login and on reconnect. Thread subscription inverts this: ThreadSubscriptionConfig defaults to ON to match the React UI Kit, so its prop is the usual way to hide it. Pin Conversation additionally needs features.ux.conversations.pinned.enabled, which is seeded in no plan. Also documents the UI Kit's own state helpers — isPinned, isSaved, isSystemPin, isConversationPinned, isSystemPinnedConversation — which have no equivalent in the Chat SDK and exist because the SDK exposes only the raw timestamps. conversations.mdx now distinguishes CometChatUIEventHandler.addConversationListener, which the page already used, from the Chat SDK's ConversationListener, which does not exist. The identical names on the two libraries are otherwise a trap. Co-Authored-By: Claude Opus 5 (1M context) --- docs.json | 2 + ui-kit/react-native/conversations.mdx | 117 ++++++++++ ui-kit/react-native/message-list.mdx | 142 ++++++++++++ ui-kit/react-native/pinned-messages.mdx | 287 ++++++++++++++++++++++++ ui-kit/react-native/saved-messages.mdx | 246 ++++++++++++++++++++ 5 files changed, 794 insertions(+) create mode 100644 ui-kit/react-native/pinned-messages.mdx create mode 100644 ui-kit/react-native/saved-messages.mdx diff --git a/docs.json b/docs.json index b631d1fa2..a2c6228d8 100644 --- a/docs.json +++ b/docs.json @@ -1169,6 +1169,8 @@ "ui-kit/react-native/group-members", "ui-kit/react-native/message-header", "ui-kit/react-native/message-list", + "ui-kit/react-native/pinned-messages", + "ui-kit/react-native/saved-messages", "ui-kit/react-native/message-composer", "ui-kit/react-native/compact-message-composer", "ui-kit/react-native/threaded-messages-header", diff --git a/ui-kit/react-native/conversations.mdx b/ui-kit/react-native/conversations.mdx index 4831585f6..e2c86e498 100644 --- a/ui-kit/react-native/conversations.mdx +++ b/ui-kit/react-native/conversations.mdx @@ -40,6 +40,7 @@ description: "Display CometChat React Native UI Kit conversations with last mess "visibility": { "hideBackButton": { "type": "boolean", "default": true }, "hideHeader": { "type": "boolean", "default": false }, + "pinConversationOptionVisibility": { "type": "boolean", "default": true }, "hideError": { "type": "boolean", "default": false }, "receiptsVisibility": { "type": "boolean", "default": true }, "hideSubmitButton": { "type": "boolean", "default": true }, @@ -410,6 +411,122 @@ Manual: deleting a conversation via the SDK directly (not through the component' --- +## Pinning Conversations + +A pinned conversation sits at the top of the list. The pin is **private to the logged-in user** — +nobody else sees it — and it syncs to that user's other devices. + + + **Off by default, and it needs a server flag that is not seeded.** Pin Conversation requires + `features.ux.conversations.pinned.enabled`, which ships enabled in no plan and must be mapped per + app. Until then every attempt rejects with `ERR_FEATURE_NOT_ACCESSIBLE`. On top of that, the kit + needs your own opt-in via `PinConversationConfig.enable(true)`. + + + + +```typescript +import { PinConversationConfig } from "@cometchat/chat-uikit-react-native"; + +// Once, at app start — defaults to off +PinConversationConfig.enable(true); +``` + + + + +```javascript +import { PinConversationConfig } from "@cometchat/chat-uikit-react-native"; + +PinConversationConfig.enable(true); +``` + + + + + +`pinConversationOptionVisibility` (default `true`) hides the option on one particular list. It is +**ANDed** with the global config, so the option renders only when both allow it. + + + +```tsx + +``` + + + + +```jsx + +``` + + + + + +### Reading pin state + + + +```typescript +import { + isConversationPinned, + isSystemPinnedConversation, +} from "@cometchat/chat-uikit-react-native"; + +if (isConversationPinned(conversation)) { + // Pinned — render the pin affordance +} + +if (isSystemPinnedConversation(conversation)) { + // An admin/global pin. A user cannot unpin one of these, + // so hide or disable the unpin control. +} +``` + + + + +```javascript +import { + isConversationPinned, + isSystemPinnedConversation, +} from "@cometchat/chat-uikit-react-native"; + +if (isConversationPinned(conversation)) { + // Pinned — render the pin affordance +} + +if (isSystemPinnedConversation(conversation)) { + // Admin/global pin — not user-unpinnable +} +``` + + + + + + + The conversation list arrives **already pin-ordered** from the server — admin pins first, then the + user's own, then everything else — and each row carries its pin attributes. You do not need a + separate fetch to render a pinned section. + + + + **Conversation pins have no real-time channel.** The **Chat SDK** exposes no `ConversationListener` + and no `onConversationPinned` / `onConversationUnpinned` callbacks, so a pin made on another device + never arrives over the socket. The list updates from the action taken on this device; re-fetch when + the screen regains focus to pick up pins made elsewhere. + + Do not confuse this with `CometChatUIEventHandler.addConversationListener`, used above — that is the + **UI Kit's own event bus** for in-app events such as `ccConversationDeleted`, and it is unrelated to + the SDK listener that does not exist. + + +See [Pin A Conversation](/sdk/react-native/pin-conversation) for the SDK methods underneath. + + ## Custom View Slots Each slot replaces a section of the default UI. Slots that accept a conversation parameter receive the `CometChat.Conversation` object for that row. diff --git a/ui-kit/react-native/message-list.mdx b/ui-kit/react-native/message-list.mdx index cac658ca5..3c21f8ef1 100644 --- a/ui-kit/react-native/message-list.mdx +++ b/ui-kit/react-native/message-list.mdx @@ -43,6 +43,11 @@ description: "Display sent and received messages with text, media, reactions, re "hideTimestamp": { "type": "boolean", "default": false }, "hideReplyOption": { "type": "boolean", "default": false }, "hideReplyInThreadOption": { "type": "boolean", "default": false }, + "hideThreadSubscriptionOption": { "type": "boolean", "default": false }, + "hidePinMessageOption": { "type": "boolean", "default": false }, + "hideUnpinMessageOption": { "type": "boolean", "default": false }, + "hideSaveMessageOption": { "type": "boolean", "default": false }, + "hideUnsaveMessageOption": { "type": "boolean", "default": false }, "hideShareMessageOption": { "type": "boolean", "default": false }, "hideEditMessageOption": { "type": "boolean", "default": false }, "hideDeleteMessageOption": { "type": "boolean", "default": false }, @@ -395,6 +400,88 @@ The component listens to these SDK events internally. No manual attachment neede --- +## Pin, Save and Thread Subscription Options + +These actions appear in the message options sheet, each with a prop to hide it. + +| Prop | Default | Hides | +| --- | --- | --- | +| `hidePinMessageOption` | `false` | Pin message | +| `hideUnpinMessageOption` | `false` | Unpin message | +| `hideSaveMessageOption` | `false` | Save message | +| `hideUnsaveMessageOption` | `false` | Unsave message | +| `hideThreadSubscriptionOption` | `false` | Follow / Unfollow thread | + + + **A `false` default does not mean the option is visible.** Pin and Save are gated twice, and both + gates must pass: + + 1. **Your opt-in** — `PinSaveConfig.enablePin(true)` / `PinSaveConfig.enableSave(true)`, called once + at app start. This defaults to **off**, so without it the options never render, whatever these + props say. + 2. **The server flag** — resolved by the kit itself at login and on every reconnect. + + Thread subscription works the other way round: `ThreadSubscriptionConfig` defaults to **on**, so the + option shows unless you opt out. That matches the React UI Kit, which has no equivalent global + switch at all. + + + + +```typescript +import { + PinSaveConfig, + ThreadSubscriptionConfig, +} from "@cometchat/chat-uikit-react-native"; + +// Once, at app start +PinSaveConfig.enablePin(true); +PinSaveConfig.enableSave(true); + +// On by default — call this only to opt OUT +ThreadSubscriptionConfig.setEnabled(false); +``` + + + + +```javascript +import { + PinSaveConfig, + ThreadSubscriptionConfig, +} from "@cometchat/chat-uikit-react-native"; + +PinSaveConfig.enablePin(true); +PinSaveConfig.enableSave(true); + +ThreadSubscriptionConfig.setEnabled(false); +``` + + + + + +The global gate and the per-instance prop are **ANDed**: use the config to switch a feature on for the +app, and the props to hide it on one particular list. + + + **Removing asks, adding does not.** Unpin and Unsave show a confirmation; Pin and Save run + immediately. The asymmetry is deliberate and shared across all CometChat UI Kits — the additive + action has its own undo one tap away, the destructive one does not. + + + + **A just-sent message may briefly refuse to pin or save.** On a moderated app the server holds a new + message at `moderation.status: "pending"` for a second or two and refuses with + `403 ERR_MESSAGE_NO_ACCESS` — the same code it returns for a genuine permission refusal. The kit + keeps the options visible and shows a transient message rather than claiming the user lacks + permission, because by the time someone opens the sheet and taps, moderation has usually finished. + + +See [Pinned Messages](/ui-kit/react-native/pinned-messages) and +[Saved Messages](/ui-kit/react-native/saved-messages) for the panels that list the results. + + ## Custom View Slots Each slot replaces a section of the default UI. @@ -1017,6 +1104,17 @@ Hides the moderation status UI. --- +### hidePinMessageOption + +Hides the "Pin message" option. Requires `PinSaveConfig.enablePin(true)` and the server flag before the option can appear at all. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + ### hideReactionOption Hides the reaction option. @@ -1050,6 +1148,17 @@ Hides the reply option. --- +### hideSaveMessageOption + +Hides the "Save message" option. Requires `PinSaveConfig.enableSave(true)` and the server flag before the option can appear at all. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + ### hideShareMessageOption Hides the share message option. @@ -1072,6 +1181,17 @@ Hides suggested messages in AI view. --- +### hideThreadSubscriptionOption + +Hides the "Follow / Unfollow thread" option. Unlike pin and save, `ThreadSubscriptionConfig` defaults to **on**, so this prop is the usual way to hide it. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + ### hideTimestamp Hides timestamps on messages. @@ -1083,6 +1203,28 @@ Hides timestamps on messages. --- +### hideUnpinMessageOption + +Hides the "Unpin message" option. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + +### hideUnsaveMessageOption + +Hides the "Unsave message" option. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + ### hideTranslateMessageOption Hides the translate message option. diff --git a/ui-kit/react-native/pinned-messages.mdx b/ui-kit/react-native/pinned-messages.mdx new file mode 100644 index 000000000..ef80becbe --- /dev/null +++ b/ui-kit/react-native/pinned-messages.mdx @@ -0,0 +1,287 @@ +--- +title: "Pinned Messages" +sidebarTitle: "Pinned Messages" +description: "Display the messages pinned in a conversation, with unpin, save, copy, share and info actions." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```javascript +import { + CometChatPinnedMessages, + PinSaveConfig, +} from "@cometchat/chat-uikit-react-native"; + +// Required once at app start — the feature is OFF until you opt in +PinSaveConfig.enablePin(true); + + navigation.goBack()} + onItemPress={(message) => scrollToMessage(message)} +/> +``` + +Props: `user`, `group`, `limit`, `onBack`, `onItemPress`, `hideUnpinMessageOption`, +`hideSaveMessageOption`, `hideUnsaveMessageOption`, `hideCopyMessageOption`, +`hideShareMessageOption`, `hideMessageInfoOption`, `ItemView`, `title`, `style`. + + + +`CometChatPinnedMessages` lists the messages pinned in one conversation, newest pin first. A pin is +conversation-wide and visible to everyone, so this panel shows the same set to every participant. + + + **The feature is off until you enable it.** `PinSaveConfig.enablePin(true)` is an explicit + integrator opt-in — without it the pin options never render, and this panel has nothing to show. + Call it once at app start, before the first screen mounts. + + +## Where It Fits + +Open it from the message header or an overflow menu, scoped to the conversation the user is in. It is +a full-screen panel with its own back affordance rather than an inline strip. + +## Minimal Render + +Pass **exactly one** of `user` or `group` — the panel is scoped to a single conversation. + + + +```tsx +import { CometChatPinnedMessages } from "@cometchat/chat-uikit-react-native"; + + navigation.goBack()} + onItemPress={(message: CometChat.BaseMessage) => scrollToMessage(message)} +/> +``` + + + + +```tsx +import { CometChatPinnedMessages } from "@cometchat/chat-uikit-react-native"; + + navigation.goBack()} + onItemPress={(message: CometChat.BaseMessage) => scrollToMessage(message)} +/> +``` + + + + + +## Enabling the feature + +Pin and Save are gated twice, and **both gates must pass** before an option renders: + +1. **Your opt-in** — `PinSaveConfig.enablePin(true)` / `PinSaveConfig.enableSave(true)`. +2. **The server flag** — resolved by the kit itself at login and on every reconnect. + + + +```typescript +import { + PinSaveConfig, + refreshPinSaveFeatures, + getPinSaveFeatures, +} from "@cometchat/chat-uikit-react-native"; + +// Once, at app start +PinSaveConfig.enablePin(true); +PinSaveConfig.enableSave(true); + +// Read what the kit resolved from the server +const features = getPinSaveFeatures(); + +// Force a re-read after flipping a flag in the dashboard, without a re-login +await refreshPinSaveFeatures(); +``` + + + + +```javascript +import { + PinSaveConfig, + refreshPinSaveFeatures, + getPinSaveFeatures, +} from "@cometchat/chat-uikit-react-native"; + +PinSaveConfig.enablePin(true); +PinSaveConfig.enableSave(true); + +const features = getPinSaveFeatures(); +refreshPinSaveFeatures(); +``` + + + + + + + You do not need to fetch the server flags yourself. The kit resolves them at login and after every + reconnect. `refreshPinSaveFeatures()` exists for the case where an admin flips a flag in the + dashboard while the app is running. + + +## Props + +| Property | Type | Default | Description | +| --- | --- | --- | --- | +| `user` | `CometChat.User` | — | Scopes the panel to a one-on-one conversation. Mutually exclusive with `group`. | +| `group` | `CometChat.Group` | — | Scopes the panel to a group. Mutually exclusive with `user`. | +| `limit` | `number` | — | Page size for the fetch. | +| `onBack` | `() => void` | — | Called when the back affordance is pressed. | +| `onItemPress` | `(message) => void` | — | Called when a row is pressed — use it to jump to the message in the list. | +| `hideUnpinMessageOption` | `boolean` | `false` | Hides Unpin in the row menu. | +| `hideSaveMessageOption` | `boolean` | `false` | Hides Save in the row menu. | +| `hideUnsaveMessageOption` | `boolean` | `false` | Hides Unsave in the row menu. | +| `hideCopyMessageOption` | `boolean` | `false` | Hides Copy. | +| `hideShareMessageOption` | `boolean` | `false` | Hides Share. | +| `hideMessageInfoOption` | `boolean` | `false` | Hides Message Info. | +| `ItemView` | `(message) => JSX.Element` | — | Replaces the default row entirely. | +| `title` | `string` | localized | Panel title. | +| `style` | `DeepPartial` | — | Style overrides. | + +## Reading pin state yourself + +The kit exports helpers so a custom `ItemView` does not have to reimplement them. The SDK has no +`isPinned()` of its own — these are UI Kit conveniences over `getPinnedAt()` and `getPinnedBy()`. + + + +```typescript +import { isPinned, isSystemPin, SYSTEM_PINNER } from "@cometchat/chat-uikit-react-native"; + +if (isPinned(message)) { + // Pinned by somebody in this conversation +} + +if (isSystemPin(message)) { + // An admin/global pin — pinnedBy is SYSTEM_PINNER ("app_system"), not a real member +} +``` + + + + +```javascript +import { isPinned, isSystemPin } from "@cometchat/chat-uikit-react-native"; + +if (isPinned(message)) { + // Pinned by somebody in this conversation +} + +if (isSystemPin(message)) { + // An admin/global pin +} +``` + + + + + +## Actions and Events + +The panel keeps itself current from the kit's event bus, so a pin made anywhere — this screen, the +message list, or another participant's device — lands here without a manual refresh. + +| Event | Fires when | +| --- | --- | +| `ccMessagePinned` / `ccMessageUnpinned` | This device pinned or unpinned. | +| `ccMessageSaved` / `ccMessageUnsaved` | This device saved or unsaved. | +| `onMessagePinned` / `onMessageUnpinned` | Another participant pinned or unpinned — broadcast to everyone. | +| `onMessageSaved` / `onMessageUnsaved` | The same user saved on another device — private multi-device. | + + + **Removing asks, adding does not.** Unpin and Unsave show a confirmation; Pin and Save run + immediately. This is deliberate and consistent across all CometChat UI Kits — the additive action + has its own undo one tap away, the destructive one does not. + + +## Common Patterns + +### Jump to the pinned message + + + +```tsx + { + navigation.goBack(); + scrollToMessageId(message.getId()); + }} +/> +``` + + + + +```jsx + { + navigation.goBack(); + scrollToMessageId(message.getId()); + }} +/> +``` + + + + + +### Read-only panel + + + +```tsx + +``` + + + + +```jsx + +``` + + + + + +--- + +## Next Steps + + + + The private, cross-conversation counterpart + + + Where the Pin and Save options are raised + + + The SDK methods underneath this component + + + Every UI Kit event, in one place + + diff --git a/ui-kit/react-native/saved-messages.mdx b/ui-kit/react-native/saved-messages.mdx new file mode 100644 index 000000000..518b6f4c1 --- /dev/null +++ b/ui-kit/react-native/saved-messages.mdx @@ -0,0 +1,246 @@ +--- +title: "Saved Messages" +sidebarTitle: "Saved Messages" +description: "Display the logged-in user's saved messages across every conversation, with unsave and jump-to-message actions." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```javascript +import { + CometChatSavedMessages, + PinSaveConfig, +} from "@cometchat/chat-uikit-react-native"; + +// Required once at app start — the feature is OFF until you opt in +PinSaveConfig.enableSave(true); + + navigation.goBack()} + onItemPress={(message, source) => openConversation(message, source)} +/> +``` + +Props: `limit`, `onBack`, `onItemPress`, `hideUnsaveMessageOption`, `ItemView`, `title`, `style`. +Note there is no `user` or `group` prop — the list is account-wide by design. + + + +`CometChatSavedMessages` lists every message the logged-in user has saved, newest save first. Unlike a +pin, a save is **private and spans conversations** — nobody else can see it, and the list is not scoped +to a single chat. + + + **The feature is off until you enable it.** `PinSaveConfig.enableSave(true)` is an explicit + integrator opt-in — without it the Save option never renders and this panel has nothing to show. + Call it once at app start. + + +## Where It Fits + +Because the list is account-wide, this panel belongs at app level — a tab, a drawer entry, or a +profile screen — not inside a single conversation. + +## Minimal Render + +There is deliberately no `user` or `group` prop. Scoping it to one conversation would defeat the point. + + + +```tsx +import { CometChatSavedMessages } from "@cometchat/chat-uikit-react-native"; + + navigation.goBack()} + onItemPress={(message: CometChat.BaseMessage, source) => { + // `source` carries the conversation the message came from + openConversation(message, source); + }} +/> +``` + + + + +```jsx +import { CometChatSavedMessages } from "@cometchat/chat-uikit-react-native"; + + navigation.goBack()} + onItemPress={(message, source) => { + openConversation(message, source); + }} +/> +``` + + + + + +## Props + +| Property | Type | Default | Description | +| --- | --- | --- | --- | +| `limit` | `number` | — | Page size for the fetch. | +| `onBack` | `() => void` | — | Called when the back affordance is pressed. | +| `onItemPress` | `(message, source) => void` | — | Called when a row is pressed. `source` identifies which conversation the message belongs to. | +| `hideUnsaveMessageOption` | `boolean` | `false` | Hides Unsave in the row menu. | +| `ItemView` | `(message) => JSX.Element` | — | Replaces the default row entirely. | +| `title` | `string` | localized | Panel title. | +| `style` | `DeepPartial` | — | Style overrides. | + +## Routing a tap back to its conversation + +Because the rows come from different conversations, `onItemPress` hands you a `source` alongside the +message. If you build a custom `ItemView`, the message itself carries the same context. + + + +```tsx + { + const conversationId = message.getConversationId(); + const receiverType = message.getReceiverType(); // "user" or "group" + const receiverId = message.getReceiverId(); + + navigateToConversation({ receiverType, receiverId, conversationId }); + }} +/> +``` + + + + +```jsx + { + navigateToConversation({ + receiverType: message.getReceiverType(), + receiverId: message.getReceiverId(), + conversationId: message.getConversationId(), + }); + }} +/> +``` + + + + + +## Reading save state yourself + + + +```typescript +import { isSaved } from "@cometchat/chat-uikit-react-native"; + +if (isSaved(message)) { + // The logged-in user saved this one +} +``` + + + + +```javascript +import { isSaved } from "@cometchat/chat-uikit-react-native"; + +if (isSaved(message)) { + // The logged-in user saved this one +} +``` + + + + + + + `savedAt` is **per-viewer**. The same message reads saved for the user who saved it and unsaved for + everybody else — so `isSaved()` is always a statement about the logged-in user, never about the + message globally. + + +## Actions and Events + +| Event | Fires when | +| --- | --- | +| `ccMessageSaved` / `ccMessageUnsaved` | This device saved or unsaved. | +| `onMessageSaved` / `onMessageUnsaved` | The same user saved on **another** device. | + + + The SDK listener never fires on the device that performed the save — that device already has the + resolved message. The panel updates from the local event there, and from the SDK event everywhere + else, so a save on a phone appears on a tablet without a refresh. + + +## Common Patterns + +### A saved-messages tab + + + +```tsx + ( + openConversation(message)} + /> + )} +/> +``` + + + + +```jsx + ( + openConversation(message)} /> + )} +/> +``` + + + + + +### Read-only list + + + +```tsx + +``` + + + + +```jsx + +``` + + + + + +--- + +## Next Steps + + + + The conversation-wide, everyone-sees-it counterpart + + + Where the Save option is raised + + + The SDK methods underneath this component + + + Every UI Kit event, in one place + + From 4c3ffd0a47f39488953300e80928a1ed40a8648e Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Mon, 7 Sep 2026 20:25:57 +0530 Subject: [PATCH 4/6] fix(react-native-docs): match the component AI reference format, and two review items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes. The two new UI Kit component pages used the SDK's javascript-snippet form of the AI Integration Quick Reference. Every other page in the Components group — conversations, users, groups, group-members, message-header, message-list, message-composer, compact-message-composer, threaded-messages-header, incoming-call, outgoing-call, call-buttons, call-logs, notification-feed — uses a structured JSON schema instead, keyed on component/package/import/description plus props grouped as data, callbacks, visibility and customization. Both pages now follow that, with a `requires` block for the PinSaveConfig opt-in and the server flag, and a `helpers` block for the kit's own state helpers. Both blocks parse as valid JSON. Review item 1 — real-time-listeners documented onMessagePinned, onMessageUnpinned, onMessageSaved and onMessageUnsaved in the Message Listener table, but the addMessageListener sample below it ended at onCardMessageReceived in both tabs. A reader copying the sample got no pin/save handling. All four are now in the TypeScript and JavaScript tabs. Review item 2 — pin-conversation passed different arguments in paired tabs: setPinnedBy(["system", "me"]) in TypeScript, setPinnedBy(["me"]) in JavaScript, which reads as a behavioural difference between the two languages. The JavaScript tab now matches the TypeScript tab and the accordion. Co-Authored-By: Claude Opus 5 (1M context) --- sdk/react-native/pin-conversation.mdx | 2 +- sdk/react-native/real-time-listeners.mdx | 24 ++++++++++ ui-kit/react-native/pinned-messages.mdx | 59 ++++++++++++++++-------- ui-kit/react-native/saved-messages.mdx | 49 ++++++++++++-------- 4 files changed, 95 insertions(+), 39 deletions(-) diff --git a/sdk/react-native/pin-conversation.mdx b/sdk/react-native/pin-conversation.mdx index 90efc0d8c..a27cffc80 100644 --- a/sdk/react-native/pin-conversation.mdx +++ b/sdk/react-native/pin-conversation.mdx @@ -199,7 +199,7 @@ conversationsRequest.fetchNext().then( ```javascript let conversationsRequest = new CometChat.ConversationsRequestBuilder() - .setPinnedBy(["me"]) + .setPinnedBy(["system", "me"]) .setLimit(30) .build(); diff --git a/sdk/react-native/real-time-listeners.mdx b/sdk/react-native/real-time-listeners.mdx index 7a8114ff3..cdfba1ec4 100644 --- a/sdk/react-native/real-time-listeners.mdx +++ b/sdk/react-native/real-time-listeners.mdx @@ -410,6 +410,18 @@ CometChat.addMessageListener( onCardMessageReceived: (cardMessage: CometChat.CardMessage) => { console.log("Card message received", cardMessage); }, + onMessagePinned: (message: CometChat.BaseMessage) => { + console.log("Message pinned", message); + }, + onMessageUnpinned: (message: CometChat.BaseMessage) => { + console.log("Message unpinned", message); + }, + onMessageSaved: (message: CometChat.BaseMessage) => { + console.log("Message saved", message); + }, + onMessageUnsaved: (message: CometChat.BaseMessage) => { + console.log("Message unsaved", message); + }, }) ); ``` @@ -466,6 +478,18 @@ CometChat.addMessageListener( onCardMessageReceived: (cardMessage) => { console.log("Card message received", cardMessage); }, + onMessagePinned: (message) => { + console.log("Message pinned", message); + }, + onMessageUnpinned: (message) => { + console.log("Message unpinned", message); + }, + onMessageSaved: (message) => { + console.log("Message saved", message); + }, + onMessageUnsaved: (message) => { + console.log("Message unsaved", message); + }, }) ); ``` diff --git a/ui-kit/react-native/pinned-messages.mdx b/ui-kit/react-native/pinned-messages.mdx index ef80becbe..3c30f1a87 100644 --- a/ui-kit/react-native/pinned-messages.mdx +++ b/ui-kit/react-native/pinned-messages.mdx @@ -6,27 +6,46 @@ description: "Display the messages pinned in a conversation, with unpin, save, c {/* TL;DR for Agents and Quick Reference */} - -```javascript -import { - CometChatPinnedMessages, - PinSaveConfig, -} from "@cometchat/chat-uikit-react-native"; - -// Required once at app start — the feature is OFF until you opt in -PinSaveConfig.enablePin(true); - - navigation.goBack()} - onItemPress={(message) => scrollToMessage(message)} -/> +```json +{ + "component": "CometChatPinnedMessages", + "package": "@cometchat/chat-uikit-react-native", + "import": "import { CometChatPinnedMessages } from \"@cometchat/chat-uikit-react-native\";", + "description": "Lists the messages pinned in a conversation, newest pin first. A pin is conversation-wide and visible to everyone.", + "requires": { + "optIn": "PinSaveConfig.enablePin(true) — called once at app start; defaults to off", + "serverFlag": "Resolved by the kit at login and on reconnect; refresh with refreshPinSaveFeatures()" + }, + "props": { + "data": { + "user": { "type": "CometChat.User", "default": "undefined", "note": "Scopes the panel to a one-on-one conversation. Mutually exclusive with group" }, + "group": { "type": "CometChat.Group", "default": "undefined", "note": "Scopes the panel to a group. Mutually exclusive with user" }, + "limit": { "type": "number", "default": "SDK default", "note": "Page size for the fetch" } + }, + "callbacks": { + "onBack": "() => void", + "onItemPress": "(message: CometChat.BaseMessage) => void" + }, + "visibility": { + "hideUnpinMessageOption": { "type": "boolean", "default": false }, + "hideSaveMessageOption": { "type": "boolean", "default": false }, + "hideUnsaveMessageOption": { "type": "boolean", "default": false }, + "hideCopyMessageOption": { "type": "boolean", "default": false }, + "hideShareMessageOption": { "type": "boolean", "default": false }, + "hideMessageInfoOption": { "type": "boolean", "default": false } + }, + "customization": { + "ItemView": "(message: CometChat.BaseMessage) => JSX.Element", + "title": { "type": "string", "default": "localized" }, + "style": "DeepPartial" + } + }, + "helpers": { + "isPinned": "(message) => boolean", + "isSystemPin": "(message) => boolean — pinnedBy is SYSTEM_PINNER (\"app_system\")" + } +} ``` - -Props: `user`, `group`, `limit`, `onBack`, `onItemPress`, `hideUnpinMessageOption`, -`hideSaveMessageOption`, `hideUnsaveMessageOption`, `hideCopyMessageOption`, -`hideShareMessageOption`, `hideMessageInfoOption`, `ItemView`, `title`, `style`. - `CometChatPinnedMessages` lists the messages pinned in one conversation, newest pin first. A pin is diff --git a/ui-kit/react-native/saved-messages.mdx b/ui-kit/react-native/saved-messages.mdx index 518b6f4c1..f9a7c4105 100644 --- a/ui-kit/react-native/saved-messages.mdx +++ b/ui-kit/react-native/saved-messages.mdx @@ -6,25 +6,38 @@ description: "Display the logged-in user's saved messages across every conversat {/* TL;DR for Agents and Quick Reference */} - -```javascript -import { - CometChatSavedMessages, - PinSaveConfig, -} from "@cometchat/chat-uikit-react-native"; - -// Required once at app start — the feature is OFF until you opt in -PinSaveConfig.enableSave(true); - - navigation.goBack()} - onItemPress={(message, source) => openConversation(message, source)} -/> +```json +{ + "component": "CometChatSavedMessages", + "package": "@cometchat/chat-uikit-react-native", + "import": "import { CometChatSavedMessages } from \"@cometchat/chat-uikit-react-native\";", + "description": "Lists every message the logged-in user has saved, newest save first. A save is private and spans all conversations, so there is no user or group prop.", + "requires": { + "optIn": "PinSaveConfig.enableSave(true) — called once at app start; defaults to off", + "serverFlag": "Resolved by the kit at login and on reconnect; refresh with refreshPinSaveFeatures()" + }, + "props": { + "data": { + "limit": { "type": "number", "default": "SDK default", "note": "Page size for the fetch" } + }, + "callbacks": { + "onBack": "() => void", + "onItemPress": "(message: CometChat.BaseMessage, source: SavedMessageSource | null) => void" + }, + "visibility": { + "hideUnsaveMessageOption": { "type": "boolean", "default": false } + }, + "customization": { + "ItemView": "(message: CometChat.BaseMessage) => JSX.Element", + "title": { "type": "string", "default": "localized" }, + "style": "DeepPartial" + } + }, + "helpers": { + "isSaved": "(message) => boolean — savedAt is per-viewer, always about the logged-in user" + } +} ``` - -Props: `limit`, `onBack`, `onItemPress`, `hideUnsaveMessageOption`, `ItemView`, `title`, `style`. -Note there is no `user` or `group` prop — the list is account-wide by design. - `CometChatSavedMessages` lists every message the logged-in user has saved, newest save first. Unlike a From d32ce177bee9bd8242841caedb5abd67def2fd17 Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Mon, 7 Sep 2026 21:19:33 +0530 Subject: [PATCH 5/6] docs(react-native): state the setLimit default for the pinned and saved lists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pages showed setLimit(50) in examples but never said what happens without it. MessagesRequestBuilder defaults to 30 and rejects anything above 100 (Constants.ts — MSGS_LIMIT 30, MSGS_MAX_LIMIT 100), so both SDK pages now say so in the same words retrieve-conversations already uses. The two UI Kit panels documented `limit` as "SDK default" with an em dash in the prop table, which told a reader nothing. Both now state 30 — the value CometChatPinnedMessages and CometChatSavedMessages actually pass (`builder.setLimit(limit ?? 30)`) — and note the 100 ceiling. Co-Authored-By: Claude Opus 5 (1M context) --- sdk/react-native/pin-message.mdx | 2 ++ sdk/react-native/save-message.mdx | 2 ++ ui-kit/react-native/pinned-messages.mdx | 4 ++-- ui-kit/react-native/saved-messages.mdx | 4 ++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/sdk/react-native/pin-message.mdx b/sdk/react-native/pin-message.mdx index bf5ff15fa..a5b713509 100644 --- a/sdk/react-native/pin-message.mdx +++ b/sdk/react-native/pin-message.mdx @@ -227,6 +227,8 @@ messagesRequest.fetchPrevious().then( 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. +The default value for `setLimit` is 30 and the max value is 100. + Page it like any other message list: `fetchPrevious()` for older rows, `fetchNext()` for newer, stopping when a call resolves `[]`. The one difference is invisible to you — the cursor rides on `pinnedAt` instead of `sentAt`, and the SDK swaps it for you. ## Check if a Message is Pinned diff --git a/sdk/react-native/save-message.mdx b/sdk/react-native/save-message.mdx index d0732fec5..558dfe91b 100644 --- a/sdk/react-native/save-message.mdx +++ b/sdk/react-native/save-message.mdx @@ -175,6 +175,8 @@ messagesRequest.fetchPrevious().then( +The default value for `setLimit` is 30 and the max value is 100. + The list comes back newest save first. Call `fetchPrevious()` again on the same object to page through older entries — this filter changes what the list contains, not how it pages. diff --git a/ui-kit/react-native/pinned-messages.mdx b/ui-kit/react-native/pinned-messages.mdx index 3c30f1a87..052476da4 100644 --- a/ui-kit/react-native/pinned-messages.mdx +++ b/ui-kit/react-native/pinned-messages.mdx @@ -20,7 +20,7 @@ description: "Display the messages pinned in a conversation, with unpin, save, c "data": { "user": { "type": "CometChat.User", "default": "undefined", "note": "Scopes the panel to a one-on-one conversation. Mutually exclusive with group" }, "group": { "type": "CometChat.Group", "default": "undefined", "note": "Scopes the panel to a group. Mutually exclusive with user" }, - "limit": { "type": "number", "default": "SDK default", "note": "Page size for the fetch" } + "limit": { "type": "number", "default": 30, "note": "Page size for the fetch; the SDK caps it at 100" } }, "callbacks": { "onBack": "() => void", @@ -155,7 +155,7 @@ refreshPinSaveFeatures(); | --- | --- | --- | --- | | `user` | `CometChat.User` | — | Scopes the panel to a one-on-one conversation. Mutually exclusive with `group`. | | `group` | `CometChat.Group` | — | Scopes the panel to a group. Mutually exclusive with `user`. | -| `limit` | `number` | — | Page size for the fetch. | +| `limit` | `number` | `30` | Page size for the fetch. The SDK rejects a value above 100. | | `onBack` | `() => void` | — | Called when the back affordance is pressed. | | `onItemPress` | `(message) => void` | — | Called when a row is pressed — use it to jump to the message in the list. | | `hideUnpinMessageOption` | `boolean` | `false` | Hides Unpin in the row menu. | diff --git a/ui-kit/react-native/saved-messages.mdx b/ui-kit/react-native/saved-messages.mdx index f9a7c4105..361932bad 100644 --- a/ui-kit/react-native/saved-messages.mdx +++ b/ui-kit/react-native/saved-messages.mdx @@ -18,7 +18,7 @@ description: "Display the logged-in user's saved messages across every conversat }, "props": { "data": { - "limit": { "type": "number", "default": "SDK default", "note": "Page size for the fetch" } + "limit": { "type": "number", "default": 30, "note": "Page size for the fetch; the SDK caps it at 100" } }, "callbacks": { "onBack": "() => void", @@ -95,7 +95,7 @@ import { CometChatSavedMessages } from "@cometchat/chat-uikit-react-native"; | Property | Type | Default | Description | | --- | --- | --- | --- | -| `limit` | `number` | — | Page size for the fetch. | +| `limit` | `number` | `30` | Page size for the fetch. The SDK rejects a value above 100. | | `onBack` | `() => void` | — | Called when the back affordance is pressed. | | `onItemPress` | `(message, source) => void` | — | Called when a row is pressed. `source` identifies which conversation the message belongs to. | | `hideUnsaveMessageOption` | `boolean` | `false` | Hides Unsave in the row menu. | From 72bddb693daf564392fc4eacf8d41e42bbc878ec Mon Sep 17 00:00:00 2001 From: Suraj Chauhan Date: Mon, 7 Sep 2026 22:22:39 +0530 Subject: [PATCH 6/6] fix(react-native-uikit-docs): the Dashboard flag enables pin/save, not a PinSaveConfig call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three pages told developers the feature is off until they call PinSaveConfig.enablePin(true) / enableSave(true) at app start. That is backwards. PinSaveFeatureGates.applyToConfigs() sets those switches itself from the app's Dashboard flags, at login and on every reconnect: PinSaveConfig.enablePin(flags.pinMessage); PinSaveConfig.enableSave(flags.saveMessage); PinConversationConfig.enable(flags.pinConversation); The sample app never references PinSaveConfig at all, and pin/save works there — which is what surfaced this. As written, the docs sent integrators looking for a missing call, and implied a feature enabled in their Dashboard would still be dark until they wrote code. Corrected on all three pages: the Dashboard setting is the gate, the kit resolves it, and PinSaveConfig is described as the read/override surface it actually is. Thread subscription keeps its own wording — it is not an app setting, so ThreadSubscriptionConfig really is a switch, defaulting to on. Co-Authored-By: Claude Opus 5 (1M context) --- ui-kit/react-native/message-list.mdx | 44 ++++++++----------------- ui-kit/react-native/pinned-messages.mdx | 33 ++++++++----------- ui-kit/react-native/saved-messages.mdx | 10 +++--- 3 files changed, 33 insertions(+), 54 deletions(-) diff --git a/ui-kit/react-native/message-list.mdx b/ui-kit/react-native/message-list.mdx index 3c21f8ef1..df3f7a37b 100644 --- a/ui-kit/react-native/message-list.mdx +++ b/ui-kit/react-native/message-list.mdx @@ -413,32 +413,22 @@ These actions appear in the message options sheet, each with a prop to hide it. | `hideThreadSubscriptionOption` | `false` | Follow / Unfollow thread | - **A `false` default does not mean the option is visible.** Pin and Save are gated twice, and both - gates must pass: + **A `false` default does not mean the option is visible.** Pin, Save and Pin Conversation are also + gated on your app's CometChat Dashboard settings. The UI Kit resolves those flags at login and on + every reconnect, so an option renders only when the Dashboard allows it *and* the prop does not + hide it. There is no start-up call to make. - 1. **Your opt-in** — `PinSaveConfig.enablePin(true)` / `PinSaveConfig.enableSave(true)`, called once - at app start. This defaults to **off**, so without it the options never render, whatever these - props say. - 2. **The server flag** — resolved by the kit itself at login and on every reconnect. - - Thread subscription works the other way round: `ThreadSubscriptionConfig` defaults to **on**, so the - option shows unless you opt out. That matches the React UI Kit, which has no equivalent global - switch at all. + Thread subscription is different: it is not an app setting, so `ThreadSubscriptionConfig` is a real + switch — and it defaults to **on**, matching the React UI Kit. The option shows unless you opt out. ```typescript -import { - PinSaveConfig, - ThreadSubscriptionConfig, -} from "@cometchat/chat-uikit-react-native"; - -// Once, at app start -PinSaveConfig.enablePin(true); -PinSaveConfig.enableSave(true); +import { ThreadSubscriptionConfig } from "@cometchat/chat-uikit-react-native"; -// On by default — call this only to opt OUT +// Thread subscription is on by default — call this only to opt OUT. +// Pin and Save need no equivalent: the kit reads their Dashboard flags at login. ThreadSubscriptionConfig.setEnabled(false); ``` @@ -446,13 +436,7 @@ ThreadSubscriptionConfig.setEnabled(false); ```javascript -import { - PinSaveConfig, - ThreadSubscriptionConfig, -} from "@cometchat/chat-uikit-react-native"; - -PinSaveConfig.enablePin(true); -PinSaveConfig.enableSave(true); +import { ThreadSubscriptionConfig } from "@cometchat/chat-uikit-react-native"; ThreadSubscriptionConfig.setEnabled(false); ``` @@ -461,8 +445,8 @@ ThreadSubscriptionConfig.setEnabled(false); -The global gate and the per-instance prop are **ANDed**: use the config to switch a feature on for the -app, and the props to hide it on one particular list. +The Dashboard flag and the per-instance prop are **ANDed**: the Dashboard decides whether a feature is +available to your app at all, and these props hide it on one particular list. **Removing asks, adding does not.** Unpin and Unsave show a confirmation; Pin and Save run @@ -1106,7 +1090,7 @@ Hides the moderation status UI. ### hidePinMessageOption -Hides the "Pin message" option. Requires `PinSaveConfig.enablePin(true)` and the server flag before the option can appear at all. +Hides the "Pin message" option. Pin Messages must also be enabled for your app in the CometChat Dashboard before the option can appear at all. | | | | --- | --- | @@ -1150,7 +1134,7 @@ Hides the reply option. ### hideSaveMessageOption -Hides the "Save message" option. Requires `PinSaveConfig.enableSave(true)` and the server flag before the option can appear at all. +Hides the "Save message" option. Save Messages must also be enabled for your app in the CometChat Dashboard before the option can appear at all. | | | | --- | --- | diff --git a/ui-kit/react-native/pinned-messages.mdx b/ui-kit/react-native/pinned-messages.mdx index 052476da4..62e46eafb 100644 --- a/ui-kit/react-native/pinned-messages.mdx +++ b/ui-kit/react-native/pinned-messages.mdx @@ -13,8 +13,8 @@ description: "Display the messages pinned in a conversation, with unpin, save, c "import": "import { CometChatPinnedMessages } from \"@cometchat/chat-uikit-react-native\";", "description": "Lists the messages pinned in a conversation, newest pin first. A pin is conversation-wide and visible to everyone.", "requires": { - "optIn": "PinSaveConfig.enablePin(true) — called once at app start; defaults to off", - "serverFlag": "Resolved by the kit at login and on reconnect; refresh with refreshPinSaveFeatures()" + "dashboardFlag": "Pin Messages must be enabled for your app in the CometChat Dashboard", + "resolution": "The kit reads the flag at login and on every reconnect — no app code required. Force a re-read with refreshPinSaveFeatures()" }, "props": { "data": { @@ -52,9 +52,9 @@ description: "Display the messages pinned in a conversation, with unpin, save, c conversation-wide and visible to everyone, so this panel shows the same set to every participant. - **The feature is off until you enable it.** `PinSaveConfig.enablePin(true)` is an explicit - integrator opt-in — without it the pin options never render, and this panel has nothing to show. - Call it once at app start, before the first screen mounts. + **Pin Messages must be enabled for your app in the CometChat Dashboard.** Until it is, the pin + options never render and this panel has nothing to show. The UI Kit reads that flag itself at + login and on every reconnect — there is no app code to write. ## Where It Fits @@ -97,10 +97,12 @@ import { CometChatPinnedMessages } from "@cometchat/chat-uikit-react-native"; ## Enabling the feature -Pin and Save are gated twice, and **both gates must pass** before an option renders: +Pin and Save are gated on your app's Dashboard settings. The UI Kit resolves those flags at login +and on every reconnect and switches the options on or off accordingly, so there is nothing to call +at start-up. -1. **Your opt-in** — `PinSaveConfig.enablePin(true)` / `PinSaveConfig.enableSave(true)`. -2. **The server flag** — resolved by the kit itself at login and on every reconnect. +`PinSaveConfig` is exported for the cases where you need to read or override that resolved state — +not as a switch you must throw. @@ -111,11 +113,7 @@ import { getPinSaveFeatures, } from "@cometchat/chat-uikit-react-native"; -// Once, at app start -PinSaveConfig.enablePin(true); -PinSaveConfig.enableSave(true); - -// Read what the kit resolved from the server +// Read what the kit resolved from the Dashboard flags const features = getPinSaveFeatures(); // Force a re-read after flipping a flag in the dashboard, without a re-login @@ -132,9 +130,6 @@ import { getPinSaveFeatures, } from "@cometchat/chat-uikit-react-native"; -PinSaveConfig.enablePin(true); -PinSaveConfig.enableSave(true); - const features = getPinSaveFeatures(); refreshPinSaveFeatures(); ``` @@ -144,9 +139,9 @@ refreshPinSaveFeatures(); - You do not need to fetch the server flags yourself. The kit resolves them at login and after every - reconnect. `refreshPinSaveFeatures()` exists for the case where an admin flips a flag in the - dashboard while the app is running. + You do not need to fetch these flags yourself, and you do not need to enable the feature in code. + The kit resolves them at login and after every reconnect. `refreshPinSaveFeatures()` exists for the + case where an admin flips a flag in the Dashboard while the app is running. ## Props diff --git a/ui-kit/react-native/saved-messages.mdx b/ui-kit/react-native/saved-messages.mdx index 361932bad..9a6199556 100644 --- a/ui-kit/react-native/saved-messages.mdx +++ b/ui-kit/react-native/saved-messages.mdx @@ -13,8 +13,8 @@ description: "Display the logged-in user's saved messages across every conversat "import": "import { CometChatSavedMessages } from \"@cometchat/chat-uikit-react-native\";", "description": "Lists every message the logged-in user has saved, newest save first. A save is private and spans all conversations, so there is no user or group prop.", "requires": { - "optIn": "PinSaveConfig.enableSave(true) — called once at app start; defaults to off", - "serverFlag": "Resolved by the kit at login and on reconnect; refresh with refreshPinSaveFeatures()" + "dashboardFlag": "Save Messages must be enabled for your app in the CometChat Dashboard", + "resolution": "The kit reads the flag at login and on every reconnect — no app code required. Force a re-read with refreshPinSaveFeatures()" }, "props": { "data": { @@ -45,9 +45,9 @@ pin, a save is **private and spans conversations** — nobody else can see it, a to a single chat. - **The feature is off until you enable it.** `PinSaveConfig.enableSave(true)` is an explicit - integrator opt-in — without it the Save option never renders and this panel has nothing to show. - Call it once at app start. + **Save Messages must be enabled for your app in the CometChat Dashboard.** Until it is, the Save + option never renders and this panel has nothing to show. The UI Kit reads that flag itself at login + and on every reconnect — there is no app code to write. ## Where It Fits