> ## 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 & Save Messages

> Let users pin important messages for everyone in a conversation and save messages privately for themselves.

## Goal

By the end of this guide you will have a chat screen where users can **pin** a message so it's highlighted for everyone in the conversation, open a panel of all pinned messages, and **save** a message privately to their own list — with a dedicated "Saved" screen to review saves across every conversation.

Pin and save are two separate concepts:

|                 | Pin                                          | Save                                         |
| --------------- | -------------------------------------------- | -------------------------------------------- |
| **Visible to**  | Everyone in the conversation                 | Only the current user                        |
| **Scope**       | One conversation                             | All conversations                            |
| **Surfaced by** | `CometChatPinnedMessages` (per conversation) | `CometChatSavedMessages` (a personal screen) |
| **Opened from** | The message header's pinned-messages action  | Your own navigation (no built-in trigger)    |

## Prerequisites

* Completed the [Integration Guide](/docs/ui-kit/react/integration-react)
* A running `CometChatProvider` setup with valid credentials
* An existing chat screen using `CometChatMessageHeader`, `CometChatMessageList`, and `CometChatMessageComposer`
* **Pin messages** and **Save messages** enabled for your app through the `features.ux.messages.pinned.enabled` and `features.ux.messages.saved.enabled` app settings. See [Core Features → Pin & Save](/docs/ui-kit/react/core-features#pin-and-save-messages).

<Note>
  The pin/unpin and save/unsave options only appear in the message options menu when the corresponding feature is enabled for your app. The UI Kit reads that setting at login, so no extra wiring is needed to show or hide the options.
</Note>

## Step 1: The Message Options

Once the features are enabled, `CometChatMessageList` automatically adds **Pin**, **Unpin**, **Save**, and **Unsave** to the message options menu — no props required. You only need the `hide*` props if you want to remove one:

*File: ChatScreen.tsx*

```tsx theme={null}
import { CometChatMessageList } from "@cometchat/chat-uikit-react";

<CometChatMessageList
  group={group}
  // Options are shown by default; pass hide* props only to remove them:
  // hidePinMessageOption
  // hideSaveMessageOption
/>
```

The **Pin/Unpin option is shown to every member** — the UI Kit does not gate it by role. Permission is enforced by the **server**: if a member isn't allowed to pin (or unpin) in that conversation, the action is rejected and the UI Kit shows a permission toast (localizable via the `action_permission_denied` key). Saving is per-user and always available. See the [Message List options](/docs/ui-kit/react/components/message-list#pin-and-save-options).

## Step 2: Open the Pinned Messages Panel

`CometChatMessageHeader` exposes a pinned-messages action in its overflow menu. Wire `onPinnedMessagesClicked` to show `CometChatPinnedMessages`, scoped to the same `user`/`group`.

*File: ChatScreen.tsx*

```tsx theme={null}
import { useState } from "react";
import { CometChat } from "@cometchat/chat-sdk-javascript";
import {
  CometChatMessageHeader,
  CometChatMessageList,
  CometChatMessageComposer,
  CometChatPinnedMessages,
} from "@cometchat/chat-uikit-react";

function ChatScreen({ group }: { group: CometChat.Group }) {
  const [showPins, setShowPins] = useState(false);

  return (
    <div style={{ display: "flex", height: "100%" }}>
      <div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
        <CometChatMessageHeader
          group={group}
          onPinnedMessagesClicked={() => setShowPins(true)}
        />
        <div style={{ flex: 1, overflow: "hidden" }}>
          <CometChatMessageList group={group} />
        </div>
        <CometChatMessageComposer group={group} />
      </div>

      {showPins && (
        <div style={{ width: "360px", borderLeft: "1px solid #e0e0e0" }}>
          <CometChatPinnedMessages
            group={group}
            onClose={() => setShowPins(false)}
            onItemClick={() => setShowPins(false)}
          />
        </div>
      )}
    </div>
  );
}
```

<Note>
  The pinned-messages action only appears in the header when pinning is enabled (the `features.ux.messages.pinned.enabled` app setting) **and** you provide `onPinnedMessagesClicked`. Use `hidePinnedMessagesOption` on the header to remove it explicitly.
</Note>

## Step 3: Add a "Saved" Screen

Saves are personal and span every conversation, so `CometChatSavedMessages` takes no `user`/`group` and is **not** opened from a built-in menu. Mount it wherever your app wants a "Saved" destination — a route, a tab, or a panel toggled from your own button.

*File: AppShell.tsx*

```tsx theme={null}
import { useState } from "react";
import { CometChatSavedMessages } from "@cometchat/chat-uikit-react";

function AppShell() {
  const [showSaved, setShowSaved] = useState(false);

  return (
    <>
      <button onClick={() => setShowSaved(true)}>Saved</button>

      {showSaved && (
        <CometChatSavedMessages onClose={() => setShowSaved(false)} />
      )}
    </>
  );
}
```

## Step 4: Pinned & Saved Indicators

Pinned and saved messages render an indicator on the bubble in the main message list, so users can see a message's status inline. This is automatic — no configuration needed. See [Message Bubble → Pinned & Saved indicators](/docs/ui-kit/react/components/message-bubble#pinned-and-saved-indicators).

<Info>
  **Live Preview** — a bubble carrying both the pinned and saved indicators.

  [Open in Storybook ↗](https://storybook.cometchat.io/react/?path=/story/components-bubbles-message-bubble--pinned-and-saved)
</Info>

<iframe src="https://storybook.cometchat.io/react/iframe.html?id=components-bubbles-message-bubble--pinned-and-saved&viewMode=story&shortcuts=false&singleStory=true" className="w-full rounded-xl" loading="lazy" style={{height: "250px", border: "1px solid #e0e0e0"}} title="CometChat Message Bubble — Pinned & Saved" allow="clipboard-write" />

## Step 5: Limits

Your app can cap how many messages may be pinned or saved. These caps are configured as app settings in the dashboard:

| Setting                                | Caps                                                                                                            |
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| features.ux.messages.pinned.limit      | Pins per conversation                                                                                           |
| features.ux.messages.saved.limit       | Saves per user                                                                                                  |
| features.ux.conversations.pinned.limit | Pinned conversations per user (see [Conversations](/docs/ui-kit/react/components/conversations#hidepinconversation)) |

When a user hits a cap, the UI Kit shows a toast explaining the limit — you don't need to handle the error yourself. The kit reads these settings at login so the toast can name the exact cap.

## Custom UI

The built-in components handle pin and save end to end — reach for this section only if you're building your **own** message or conversation UI and want the same behavior.

Drive pin/save directly with the SDK calls, then publish an optimistic UI event so the built-in surfaces (the message list, the Pinned and Saved panels, the conversation list) stay in sync with your action:

| Action               | SDK call                                  | Publish afterwards                                              |
| -------------------- | ----------------------------------------- | --------------------------------------------------------------- |
| Pin a message        | `CometChat.pinMessage(messageId)`         | `ui:message/pin-changed` `{ message, pinned: true }`            |
| Unpin a message      | `CometChat.unpinMessage(messageId)`       | `ui:message/pin-changed` `{ message, pinned: false }`           |
| Save a message       | `CometChat.saveMessage(messageId)`        | `ui:message/save-changed` `{ message, saved: true }`            |
| Unsave a message     | `CometChat.unsaveMessage(messageId)`      | `ui:message/save-changed` `{ message, saved: false }`           |
| Pin a conversation   | `CometChat.pinConversation(with, type)`   | `ui:conversation/pin-changed` `{ conversation, pinned: true }`  |
| Unpin a conversation | `CometChat.unpinConversation(with, type)` | `ui:conversation/pin-changed` `{ conversation, pinned: false }` |

For a conversation, `with`/`type` are the peer's UID (or the group's GUID) and `"user"` / `"group"` — not the `conversationId`.

Read the current state off the message or conversation object: `isPinned()` and `isSaved()` return booleans, and `getPinnedAt()` / `getPinnedBy()` / `getSavedAt()` return the details (or `undefined` when unset). The *presence* of the value is what "pinned" / "saved" means, so branch on `isPinned()` / `isSaved()` rather than comparing timestamps.

```tsx theme={null}
import { CometChat } from "@cometchat/chat-sdk-javascript";
import { usePublishEvent } from "@cometchat/chat-uikit-react";

function PinButton({ message }: { message: CometChat.BaseMessage }) {
  const publish = usePublishEvent();

  const togglePin = async () => {
    const id = message.getId();
    try {
      if (message.isPinned()) {
        await CometChat.unpinMessage(id);
        publish({ type: "ui:message/pin-changed", message, pinned: false });
      } else {
        await CometChat.pinMessage(id);
        publish({ type: "ui:message/pin-changed", message, pinned: true });
      }
    } catch (error) {
      // The server enforces permission and the per-app cap — surface a message here.
      console.error("Pin failed", error);
    }
  };

  return <button onClick={togglePin}>{message.isPinned() ? "Unpin" : "Pin"}</button>;
}
```

Permission and the per-app caps are enforced by the **server**, so wrap the calls in `try/catch` and surface a message on rejection — see [Limits](#step-5-limits) and the permission behavior in [Step 1](#step-1-the-message-options). Publishing the `ui:` events above is what keeps the message list and the Pinned/Saved panels in step with your custom action. For the full event list, see the [Event System](/docs/ui-kit/react/event-system#pin-and-save).

## Complete Example

*File: App.tsx*

```tsx theme={null}
import { useState } from "react";
import { CometChat } from "@cometchat/chat-sdk-javascript";
import {
  CometChatProvider,
  CometChatConversations,
  CometChatMessageHeader,
  CometChatMessageList,
  CometChatMessageComposer,
  CometChatPinnedMessages,
  CometChatSavedMessages,
} from "@cometchat/chat-uikit-react";

function ChatWithPinSave() {
  const [user, setUser] = useState<CometChat.User | null>(null);
  const [group, setGroup] = useState<CometChat.Group | null>(null);
  const [showPins, setShowPins] = useState(false);
  const [showSaved, setShowSaved] = useState(false);

  function handleConversationClick(conversation: CometChat.Conversation) {
    setShowPins(false);
    const entity = conversation.getConversationWith();
    if (entity instanceof CometChat.User) {
      setUser(entity);
      setGroup(null);
    } else if (entity instanceof CometChat.Group) {
      setGroup(entity);
      setUser(null);
    }
  }

  return (
    <div style={{ display: "flex", height: "100vh" }}>
      {/* Conversations sidebar */}
      <div style={{ width: "300px", borderRight: "1px solid #e0e0e0", display: "flex", flexDirection: "column" }}>
        <button onClick={() => setShowSaved(true)}>Saved messages</button>
        <CometChatConversations onItemClick={handleConversationClick} />
      </div>

      {/* Main message panel */}
      <div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
        {(user || group) && (
          <>
            <CometChatMessageHeader
              user={user ?? undefined}
              group={group ?? undefined}
              onPinnedMessagesClicked={() => setShowPins(true)}
            />
            <div style={{ flex: 1, overflow: "hidden" }}>
              <CometChatMessageList user={user ?? undefined} group={group ?? undefined} />
            </div>
            <CometChatMessageComposer user={user ?? undefined} group={group ?? undefined} />
          </>
        )}
      </div>

      {/* Pinned messages panel */}
      {showPins && (user || group) && (
        <div style={{ width: "360px", borderLeft: "1px solid #e0e0e0" }}>
          <CometChatPinnedMessages
            user={user ?? undefined}
            group={group ?? undefined}
            onClose={() => setShowPins(false)}
            onItemClick={() => setShowPins(false)}
          />
        </div>
      )}

      {/* Saved messages screen */}
      {showSaved && (
        <div style={{ width: "360px", borderLeft: "1px solid #e0e0e0" }}>
          <CometChatSavedMessages onClose={() => setShowSaved(false)} />
        </div>
      )}
    </div>
  );
}

function App() {
  return (
    <CometChatProvider>
      <ChatWithPinSave />
    </CometChatProvider>
  );
}

export default App;
```

## Next Steps

* [Pinned Messages](/docs/ui-kit/react/components/pinned-messages) — configure the pinned-messages panel
* [Saved Messages](/docs/ui-kit/react/components/saved-messages) — configure the saved-messages screen
* [Message List](/docs/ui-kit/react/components/message-list) — toggle the pin/save message options
* [Conversations](/docs/ui-kit/react/components/conversations#hidepinconversation) — let users pin whole conversations
