diff --git a/docs.json b/docs.json
index f17ba2a8b..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",
@@ -3540,10 +3542,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..a27cffc80
--- /dev/null
+++ b/sdk/react-native/pin-conversation.mdx
@@ -0,0 +1,396 @@
+---
+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(["system", "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 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
+
+
+
+```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..a5b713509
--- /dev/null
+++ b/sdk/react-native/pin-message.mdx
@@ -0,0 +1,426 @@
+---
+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: number = 100;
+
+CometChat.pinMessage(messageId).then(
+ (message: CometChat.BaseMessage) => {
+ console.log("Message pinned:", message);
+ },
+ (error: CometChat.CometChatException) => {
+ console.log("Failed to pin message:", error);
+ }
+);
+```
+
+
+
+
+```javascript
+let messageId = 100;
+
+CometChat.pinMessage(messageId).then(
+ (message) => {
+ console.log("Message pinned:", message);
+ },
+ (error) => {
+ console.log("Failed to pin message:", error);
+ }
+);
+```
+
+
+
+
+
+Pinning is **idempotent**, and a message has a single pinner: re-pinning an already pinned message updates `pinnedBy` and `pinnedAt` to the most recent pinner rather than failing.
+
+
+ **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
+ 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.
+
+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
+
+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..cdfba1ec4 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`:
@@ -406,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);
+ },
})
);
```
@@ -462,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);
+ },
})
);
```
@@ -491,6 +519,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..558dfe91b
--- /dev/null
+++ b/sdk/react-native/save-message.mdx
@@ -0,0 +1,382 @@
+---
+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: number = 100;
+
+CometChat.saveMessage(messageId).then(
+ (message: CometChat.BaseMessage) => {
+ console.log("Message saved:", message);
+ },
+ (error: CometChat.CometChatException) => {
+ console.log("Failed to save message:", error);
+ }
+);
+```
+
+
+
+
+```javascript
+let messageId = 100;
+
+CometChat.saveMessage(messageId).then(
+ (message) => {
+ console.log("Message saved:", message);
+ },
+ (error) => {
+ console.log("Failed to save message:", error);
+ }
+);
+```
+
+
+
+
+
+Saving is idempotent — saving an already saved message succeeds rather than failing. 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(
+ (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 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.
+
+
+ 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.
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..df3f7a37b 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,72 @@ 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, 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.
+
+ 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 { ThreadSubscriptionConfig } from "@cometchat/chat-uikit-react-native";
+
+// 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);
+```
+
+
+
+
+```javascript
+import { ThreadSubscriptionConfig } from "@cometchat/chat-uikit-react-native";
+
+ThreadSubscriptionConfig.setEnabled(false);
+```
+
+
+
+
+
+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
+ 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 +1088,17 @@ Hides the moderation status UI.
---
+### hidePinMessageOption
+
+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.
+
+| | |
+| --- | --- |
+| Type | `boolean` |
+| Default | `false` |
+
+---
+
### hideReactionOption
Hides the reaction option.
@@ -1050,6 +1132,17 @@ Hides the reply option.
---
+### hideSaveMessageOption
+
+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.
+
+| | |
+| --- | --- |
+| Type | `boolean` |
+| Default | `false` |
+
+---
+
### hideShareMessageOption
Hides the share message option.
@@ -1072,6 +1165,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 +1187,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..62e46eafb
--- /dev/null
+++ b/ui-kit/react-native/pinned-messages.mdx
@@ -0,0 +1,301 @@
+---
+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 */}
+
+```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": {
+ "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": {
+ "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": 30, "note": "Page size for the fetch; the SDK caps it at 100" }
+ },
+ "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\")"
+ }
+}
+```
+
+
+`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.
+
+
+ **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
+
+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 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.
+
+`PinSaveConfig` is exported for the cases where you need to read or override that resolved state —
+not as a switch you must throw.
+
+
+
+```typescript
+import {
+ PinSaveConfig,
+ refreshPinSaveFeatures,
+ getPinSaveFeatures,
+} from "@cometchat/chat-uikit-react-native";
+
+// 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
+await refreshPinSaveFeatures();
+```
+
+
+
+
+```javascript
+import {
+ PinSaveConfig,
+ refreshPinSaveFeatures,
+ getPinSaveFeatures,
+} from "@cometchat/chat-uikit-react-native";
+
+const features = getPinSaveFeatures();
+refreshPinSaveFeatures();
+```
+
+
+
+
+
+
+ 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
+
+| 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` | `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. |
+| `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..9a6199556
--- /dev/null
+++ b/ui-kit/react-native/saved-messages.mdx
@@ -0,0 +1,259 @@
+---
+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 */}
+
+```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": {
+ "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": {
+ "limit": { "type": "number", "default": 30, "note": "Page size for the fetch; the SDK caps it at 100" }
+ },
+ "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"
+ }
+}
+```
+
+
+`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.
+
+
+ **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
+
+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` | `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. |
+| `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
+
+