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

# Pin A Message

> Pin and unpin messages in a conversation, fetch the pinned list, and listen for pin events with the CometChat React Native SDK.

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

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.

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

## Pin a Message

Call `pinMessage()` with the message's ID. It resolves with the full updated message, with `pinnedAt` and `pinnedBy` set.

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

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

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

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.

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

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

## Unpin a Message

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

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

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.

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

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

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

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

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.

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

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

| Method          | Returns                                                           |
| --------------- | ----------------------------------------------------------------- |
| `getPinnedAt()` | The pin timestamp, or `undefined` when the message is not pinned. |
| `getPinnedBy()` | The UID of the most recent pinner, or `undefined`.                |

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

## Real-time Pin Events

Pin and unpin are broadcast to everyone in the conversation. Add the callbacks to your existing `MessageListener`.

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

  <Tab title="JavaScript">
    ```javascript theme={null}
    CometChat.addMessageListener(
      "UNIQUE_LISTENER_ID",
      new CometChat.MessageListener({
        onMessagePinned: (message) => {
          console.log("Message pinned:", message);
        },
        onMessageUnpinned: (message) => {
          console.log("Message unpinned:", message);
        },
      })
    );
    ```
  </Tab>
</Tabs>

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:

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    CometChat.removeMessageListener("UNIQUE_LISTENER_ID");
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    CometChat.removeMessageListener("UNIQUE_LISTENER_ID");
    ```
  </Tab>
</Tabs>

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

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

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

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.

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

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

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

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

<CardGroup cols={2}>
  <Card title="Save A Message" icon="bookmark" href="/docs/sdk/react-native/save-message">
    Bookmark a message privately, across conversations
  </Card>

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

  <Card title="All Real Time Listeners" icon="tower-broadcast" href="/docs/sdk/react-native/real-time-listeners">
    Every listener the SDK exposes, in one place
  </Card>

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