> ## Documentation Index
> Fetch the complete documentation index at: https://www.cometchat.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Save A Message

> Save and unsave messages privately, fetch the saved list across conversations, and listen for save events with the CometChat React Native SDK.

<Accordion title="AI Integration Quick Reference">
  ```javascript theme={null}
  // 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
  ```
</Accordion>

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.

<Note>
  `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.
</Note>

## Save a Message

Call `saveMessage()` with the message's ID. It resolves with the full updated message, with `savedAt` set.

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    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);
      }
    );
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    let messageId = 100;

    CometChat.saveMessage(messageId).then(
      (message) => {
        console.log("Message saved:", message);
      },
      (error) => {
        console.log("Failed to save message:", error);
      }
    );
    ```
  </Tab>
</Tabs>

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

<Warning>
  **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.
</Warning>

## Unsave a Message

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    CometChat.unsaveMessage(100).then(
      (message: CometChat.BaseMessage) => {
        console.log("Message unsaved:", message);
      },
      (error: CometChat.CometChatException) => {
        console.log("Failed to unsave message:", error);
      }
    );
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    CometChat.unsaveMessage(100).then(
      (message) => {
        console.log("Message unsaved:", message);
      },
      (error) => {
        console.log("Failed to unsave message:", error);
      }
    );
    ```
  </Tab>
</Tabs>

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.

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    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);
      }
    );
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    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);
      }
    );
    ```
  </Tab>
</Tabs>

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.

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

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.

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    messages.forEach((message: CometChat.BaseMessage) => {
      console.log(
        message.getConversationId(),
        message.getReceiverType(), // "user" or "group"
        message.getReceiverId()
      );
    });
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    messages.forEach((message) => {
      console.log(
        message.getConversationId(),
        message.getReceiverType(), // "user" or "group"
        message.getReceiverId()
      );
    });
    ```
  </Tab>
</Tabs>

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

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    const isSaved: boolean = message.getSavedAt() !== undefined;

    if (isSaved) {
      console.log("Saved at:", message.getSavedAt());
    }
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    const isSaved = message.getSavedAt() !== undefined;

    if (isSaved) {
      console.log("Saved at:", message.getSavedAt());
    }
    ```
  </Tab>
</Tabs>

| Method         | Returns                                                            |
| -------------- | ------------------------------------------------------------------ |
| `getSavedAt()` | The save timestamp, or `undefined` when the user has not saved it. |

<Warning>
  Do not port `isSaved()` from the JavaScript SDK — it does not exist in the
  React Native SDK. Derive it from `getSavedAt()` as shown above.
</Warning>

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

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    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);
        },
      })
    );
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    CometChat.addMessageListener(
      "UNIQUE_LISTENER_ID",
      new CometChat.MessageListener({
        onMessageSaved: (message) => {
          console.log("Message saved:", message);
        },
        onMessageUnsaved: (message) => {
          console.log("Message unsaved:", message);
        },
      })
    );
    ```
  </Tab>
</Tabs>

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

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

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    let limit: number | null = await CometChat.getSaveMessageLimit();

    if (limit !== null && savedCount >= limit) {
      // Disable the save control instead of letting the user hit the error
    }
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    CometChat.getSaveMessageLimit().then((limit) => {
      if (limit !== null && savedCount >= limit) {
        // Disable the save control instead of letting the user hit the error
      }
    });
    ```
  </Tab>
</Tabs>

It resolves to `null` when the app settings carry no value — show generic copy in that case rather than guessing a number.

## Feature Availability

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    let enabled: boolean = await CometChat.isSaveMessageEnabled();

    if (enabled) {
      // Show the Save option
    }
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    CometChat.isSaveMessageEnabled().then((enabled) => {
      if (enabled) {
        // Show the Save option
      }
    });
    ```
  </Tab>
</Tabs>

This resolves `false` rather than rejecting when the flag is missing or settings are unavailable.

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Pin A Message" icon="thumbtack" href="/docs/sdk/react-native/pin-message">
    Highlight a message for everyone in a conversation
  </Card>

  <Card title="Pin A Conversation" icon="list" href="/docs/sdk/react-native/pin-conversation">
    Pin a conversation to the top of the list
  </Card>

  <Card title="Retrieve Conversations" icon="comments" href="/docs/sdk/react-native/retrieve-conversations">
    Fetch and order the conversation list
  </Card>

  <Card title="Additional Message Filtering" icon="filter" href="/docs/sdk/react-native/additional-message-filtering">
    Filter messages by saved, pinned, type, tags and more
  </Card>
</CardGroup>
