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

> Pin messages for everyone in a conversation, save messages privately, and pin conversations to the top of the list with the CometChat Angular UIKit.

<Accordion title="AI Integration Quick Reference">
  | Field           | Value                                                                                                                                                                                                                                                                                      |
  | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
  | Package         | `@cometchat/chat-uikit-angular`                                                                                                                                                                                                                                                            |
  | Key components  | `cometchat-message-list`, `cometchat-pinned-messages`, `cometchat-saved-messages`, `cometchat-conversations`                                                                                                                                                                               |
  | Feature gate    | App settings `features.ux.messages.pinned.enabled` / `features.ux.messages.saved.enabled` / `features.ux.conversations.pinned.enabled`, read via `CometChat.isPinMessageEnabled()` / `isSaveMessageEnabled()` / `isPinConversationEnabled()` and overridable via `COMETCHAT_GLOBAL_CONFIG` |
  | Service         | `PinSaveService`                                                                                                                                                                                                                                                                           |
  | Events          | `CometChatPinSaveEvents` — `ccMessagePinned`, `ccMessageUnpinned`, `ccMessageSaved`, `ccMessageUnsaved`                                                                                                                                                                                    |
  | State           | `message.getPinnedAt()`, `getPinnedBy()`, `getSavedAt()`; `conversation.isPinned()`                                                                                                                                                                                                        |
  | SDK requirement | `@cometchat/chat-sdk-javascript` **4.2.0 or later** — the first release exposing the pin and save APIs                                                                                                                                                                                     |
  | Related         | [Pinned Messages](/docs/ui-kit/angular/components/cometchat-pinned-messages), [Saved Messages](/docs/ui-kit/angular/components/cometchat-saved-messages), [All Guides](/docs/ui-kit/angular/guides/guides-overview)                                                                                       |
</Accordion>

The UI Kit ships three related but distinct actions:

| Action               | Scope                                      | Who sees it                  |
| :------------------- | :----------------------------------------- | :--------------------------- |
| **Pin message**      | One conversation                           | Everyone in the conversation |
| **Save message**     | The acting user, across every conversation | Only that user               |
| **Pin conversation** | The acting user's conversation list        | Only that user               |

That split drives everything below: a pin is a shared act and is permission-gated, while a save is private and is not.

Before starting, complete the [Integration Guide](/docs/ui-kit/angular/integration).

***

## Enabling the Feature

Pin Message and Save Message are gated by **app settings** that CometChat provisions server-side. The UI Kit resolves them once per session through the Chat SDK:

| App setting                                | Chat SDK accessor                      |
| ------------------------------------------ | -------------------------------------- |
| `features.ux.messages.pinned.enabled`      | `CometChat.isPinMessageEnabled()`      |
| `features.ux.messages.saved.enabled`       | `CometChat.isSaveMessageEnabled()`     |
| `features.ux.conversations.pinned.enabled` | `CometChat.isPinConversationEnabled()` |

While a setting is off, the SDK reports the feature disabled and none of that feature's surfaces render.

### Development override

Because the flags are provisioned server-side, "flag off" is indistinguishable from "not built" during development. `COMETCHAT_GLOBAL_CONFIG` can force either surface on or off:

```typescript expandable theme={null}
import { ApplicationConfig } from '@angular/core';
import { COMETCHAT_GLOBAL_CONFIG, GlobalConfig } from '@cometchat/chat-uikit-angular';

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: COMETCHAT_GLOBAL_CONFIG,
      useValue: {
        enablePinMessage: true,
        enableSaveMessage: true,
      } as GlobalConfig,
    },
  ],
};
```

<Warning>
  Leave both unset in production. The app settings are the right source of truth; an explicit `true` here shows options the backend may still reject.
</Warning>

Pinning a **conversation** is gated the same way, on `features.ux.conversations.pinned.enabled`. It additionally requires the installed Chat SDK to expose `pinConversation` / `unpinConversation`, and is hidden with `[hidePinConversation]="true"`.

***

## Pinning and Saving a Message

Both actions live in the message context menu, gathered under an **Organise ▸** flyout so two related actions cost one row in an already long menu. The flyout is omitted entirely when neither action applies.

```html expandable theme={null}
<cometchat-message-list
  [group]="group"
  [hidePinMessageOption]="false"
  [hideUnpinMessageOption]="false"
  [hideSaveMessageOption]="false"
  [hideUnsaveMessageOption]="false">
</cometchat-message-list>
```

| Input                     | Default | Hides                                  |
| :------------------------ | :------ | :------------------------------------- |
| `hidePinMessageOption`    | `false` | **Pin message** on an unpinned message |
| `hideUnpinMessageOption`  | `false` | **Unpin message** on a pinned one      |
| `hideSaveMessageOption`   | `false` | **Save message** on an unsaved message |
| `hideUnsaveMessageOption` | `false` | **Unsave message** on a saved one      |

The menu shows Pin **or** Unpin — never a toggling third state — because the presence of `pinnedAt` *is* the boolean. The same holds for `savedAt`.

### Eligibility

A message can carry a pin or a save unless it is deleted, still in flight, held by moderation, or an `action` category message ("X joined the group" is not something anyone pins). Thread replies **are** eligible: the backend accepts them and returns the parent for context, so the option belongs inside the thread view too.

### Permissions

Pinning is conversation-wide, and the **server is the sole authority** on who may do it. There is no client-side role gate: Pin is offered to every member, and a member without the permission is refused with `ERR_ACTION_NOT_ALLOWED` — the optimistic flip then reverts with an explanatory toast.

Saving has no gate at all: it is private to the acting user and changes nothing anyone else can see.

<Note>
  Do not rely on the UI to withhold Pin or Unpin from a participant — it does not. `getScope()` is a membership property and is frequently undefined on a `Group` derived from a conversation, so a client-side allow-list would hide Pin from admins whose scope simply has not loaded yet. The one action withheld on the client is Unpin on a system pin, which the server refuses for every member.
</Note>

### Confirmation

Pinning and saving run immediately. **Unpinning and unsaving ask first** — unpinning acts for everyone in the chat, and an unsave can drop the only pointer the user had to a message buried far up the history.

### Limits

Each feature carries a cap, configured per app and read from app settings:

| App setting                              | Caps                             | Chat SDK accessor                         |
| ---------------------------------------- | -------------------------------- | ----------------------------------------- |
| `features.ux.messages.pinned.limit`      | Pinned messages per conversation | `CometChat.getPinnedMessagesLimit()`      |
| `features.ux.messages.saved.limit`       | Saved messages per user          | `CometChat.getSavedMessagesLimit()`       |
| `features.ux.conversations.pinned.limit` | Pinned conversations per user    | `CometChat.getPinnedConversationsLimit()` |

Every accessor above resolves asynchronously — `await` it or use `.then()`.

When a user reaches a cap, the UI Kit shows a toast naming the configured number ("You can only pin 5 messages. Unpin one to pin another."). You do not need to handle the rejection yourself. Where an app configures no cap, the toast falls back to generic copy rather than guessing a figure.

<Note>
  The caps are read once per session and cached, so the same number is available before a user reaches it — use `PinSaveService.getConfiguredLimit()` to disable a control ahead of the rejection. See [Acting Programmatically](#acting-programmatically).
</Note>

An app may also **system-pin** messages and conversations app-wide, capped separately through `features.ux.messages.pinned.system.limit` and `features.ux.conversations.pinned.system.limit` — read through `CometChat.getSystemPinnedMessagesLimit()` and `CometChat.getSystemPinnedConversationsLimit()`. System pins belong to no member: they sort above user pins and cannot be lifted from the UI.

***

## Indicators

Once a message is pinned or saved, the bubble marks it in its status-info footer:

* **Pin marker** — shown to everyone, because `pinnedAt` is conversation-wide
* **Bookmark marker** — shown only to the user who saved it; `savedAt` is simply not present in anyone else's copy

A batched media message normally shows a footer on its last bubble only. A pinned or saved bubble keeps its footer regardless, since hiding it would hide the very badge that explains the mark.

```css expandable theme={null}
/* Both markers */
.cometchat-message-bubble__pinned-indicator,
.cometchat-message-bubble__saved-indicator {
  background-color: var(--cometchat-message-bubble-meta-indicator-color, var(--cometchat-text-color-secondary));
}

/* Outgoing bubbles invert them */
.cometchat-message-bubble__wrapper--outgoing .cometchat-message-bubble__pinned-indicator {
  background-color: var(--cometchat-message-bubble-meta-indicator-color-outgoing, var(--cometchat-text-color-white));
}
```

***

## Viewing Pinned and Saved Messages

Two panels list what has been marked. They are separate components — see their own pages for the full API.

### Pinned messages

Conversation-scoped, shared, and permission-aware. Add the entry point to the message header:

```html expandable theme={null}
<cometchat-message-header
  [group]="group"
  [showPinnedMessagesOption]="true"
  (pinnedMessagesClick)="showPinnedPanel = true">
</cometchat-message-header>

@if (showPinnedPanel) {
  <cometchat-pinned-messages
    [group]="group"
    (messageClick)="scrollToMessage($event)"
    (closeClick)="showPinnedPanel = false">
  </cometchat-pinned-messages>
}
```

See [CometChatPinnedMessages](/docs/ui-kit/angular/components/cometchat-pinned-messages).

### Saved messages

Per-user and cross-conversation, so it belongs in your app chrome rather than in a chat header — hanging it off one conversation would misrepresent what it contains.

```html expandable theme={null}
<cometchat-saved-messages
  (messageClick)="openSourceConversation($event)"
  (closeClick)="showSavedPanel = false">
</cometchat-saved-messages>
```

See [CometChatSavedMessages](/docs/ui-kit/angular/components/cometchat-saved-messages).

***

## Pinning a Conversation

`CometChatConversations` adds a **Pin conversation** / **Unpin conversation** entry to each row's context menu, above Delete — the safe, reversible action comes first.

```html expandable theme={null}
<cometchat-conversations
  [hidePinConversation]="false"
  (itemClick)="onConversationClick($event)">
</cometchat-conversations>
```

### Ordering

Pinned conversations are lifted above unpinned ones. The partition is **stable**: recency still decides the order among pinned chats and among unpinned ones, so a pinned chat with a new message still rises to the top of its block, and one message in an unpinned chat can never push the pinned block down.

### Reading the state

```typescript theme={null}
const isPinned = conversation.isPinned?.();
```

The presence of `pinnedAt` is the boolean here too — an unpinned conversation carries no key at all. `isPinned()` covers both a personal pin and an app-wide one (`pinnedBy === "app_system"`).

### Behavior

* Pinning runs straight away; **unpinning asks for confirmation**, because a pin is a deliberate arrangement of the list and a misplaced click should not undo it
* The row shows a pin marker, and the state reaches screen readers through the row's accessible label, since the marker itself is `aria-hidden`
* The pin cap is server-owned and read once per session through `CometChat.getPinnedConversationsLimit()`; exceeding it shows "You can only pin N chats. Unpin one to pin another." with that number

***

## Reacting to Changes

`CometChatPinSaveEvents` is the channel that keeps bubbles, both panels, and any surface of your own in agreement without a refetch.

| Event               | Reach                 | Description                                                  |
| :------------------ | :-------------------- | :----------------------------------------------------------- |
| `ccMessagePinned`   | Broadcast             | A message was pinned — everyone in the conversation hears it |
| `ccMessageUnpinned` | Broadcast             | A message was unpinned                                       |
| `ccMessageSaved`    | Private, multi-device | The logged-in user saved a message                           |
| `ccMessageUnsaved`  | Private, multi-device | The logged-in user unsaved a message                         |

```typescript expandable theme={null}
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CometChatPinSaveEvents } from '@cometchat/chat-uikit-angular';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-pin-listener',
  standalone: true,
  template: `<!-- your template -->`,
})
export class PinListenerComponent implements OnInit, OnDestroy {
  private subscriptions = new Subscription();

  ngOnInit(): void {
    this.subscriptions.add(
      CometChatPinSaveEvents.ccMessagePinned.subscribe(({ message }) => {
        // Swap your copy of the message wholesale — do not patch fields.
        this.replaceMessage(message);
      })
    );

    this.subscriptions.add(
      CometChatPinSaveEvents.ccMessageUnpinned.subscribe(({ message }) => {
        this.replaceMessage(message);
      })
    );
  }

  ngOnDestroy(): void {
    this.subscriptions.unsubscribe();
  }

  private replaceMessage(message: unknown): void {}
}
```

<Warning>
  Every payload carries the **full updated message**. Swap your copy rather than patching fields: `pinnedAt` and `savedAt` are present-only-when-set and are *cleared*, never zeroed, on unpin and unsave — so a partial patch leaves a stale timestamp behind and the marker never disappears.
</Warning>

The payload carries the message and nothing else — there is no `source` discriminator. That distinction is what the two tiers are for: `ccMessagePinned` and its siblings are **server truth** (a confirmed write or a realtime frame), while `ccMessagePinChanged` and `ccMessageSaveChanged` are **this client's optimism** (a flip applied before the server answered, and its reversal if the write failed). Most surfaces want both, so subscribe to the merged `pinned$` / `unpinned$` / `saved$` / `unsaved$` observables rather than to the raw subjects.

***

## Acting Programmatically

`PinSaveService` is provided in root and owns the behavior for every surface.

```typescript expandable theme={null}
import { Component, inject } from '@angular/core';
import { CometChat } from '@cometchat/chat-sdk-javascript';
import { PinSaveService } from '@cometchat/chat-uikit-angular';

@Component({
  selector: 'app-custom-pin-button',
  standalone: true,
  template: `<button (click)="togglePin()">Pin</button>`,
})
export class CustomPinButtonComponent {
  private readonly pinSave = inject(PinSaveService);
  message!: CometChat.BaseMessage;

  async togglePin(): Promise<void> {
    const action = this.pinSave.isPinned(this.message) ? 'unpin' : 'pin';
    const updated = await this.pinSave.run(action, this.message);
    // `null` means the call failed; the service has already reverted and shown a toast.
  }
}
```

| Member                       | Returns                                  | Description                                                                                                                                          |
| :--------------------------- | :--------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- |
| `isSupported()`              | `boolean`                                | Whether the installed Chat SDK exposes the pin/save API at all                                                                                       |
| `isPinEnabled()`             | `Promise<boolean>`                       | `features.ux.messages.pinned.enabled`, cached per session                                                                                            |
| `isSaveEnabled()`            | `Promise<boolean>`                       | `features.ux.messages.saved.enabled`, cached per session                                                                                             |
| `isPinConversationEnabled()` | `Promise<boolean>`                       | `features.ux.conversations.pinned.enabled`, cached per session                                                                                       |
| `isPinned(message)`          | `boolean`                                | Presence of `pinnedAt`                                                                                                                               |
| `isSaved(message)`           | `boolean`                                | Presence of `savedAt`, scoped to the viewer                                                                                                          |
| `pinnedBy(message)`          | `string \| null`                         | Who pinned it. `app_system` means an admin or global pin                                                                                             |
| `isSystemPin(message)`       | `boolean`                                | Whether the pin came from the app rather than a person                                                                                               |
| `run(action, message)`       | `Promise<CometChat.BaseMessage \| null>` | Performs `'pin' \| 'unpin' \| 'save' \| 'unsave'`. Returns the updated message, or `null` when it failed                                             |
| `getConfiguredLimit(action)` | `Promise<number \| null>`                | The app's cap for that action, or `null` when none is configured. Read once and reused — use it to disable a control before the user reaches the cap |

`run()` flips the message optimistically, reconciles against the authoritative copy the server returns, and restores the pre-call attributes on failure — so a rejected pin never leaves a pin showing. One request per message is on the wire at a time, so a double-tap cannot race itself.

***

## Localization

| Key                                  | English (US)                                                      |
| :----------------------------------- | :---------------------------------------------------------------- |
| `message_list_option_organise`       | Organise                                                          |
| `message_list_option_pin_message`    | Pin message                                                       |
| `message_list_option_unpin_message`  | Unpin message                                                     |
| `message_list_option_save_message`   | Save message                                                      |
| `message_list_option_unsave_message` | Unsave message                                                    |
| `message_pinned_toast`               | Message pinned                                                    |
| `message_unpinned_toast`             | Message unpinned                                                  |
| `message_saved_toast`                | Message saved                                                     |
| `message_unsaved_toast`              | Message unsaved                                                   |
| `pinned_messages_title`              | `{count} Pinned Messages`                                         |
| `pinned_messages_menu`               | Pinned messages                                                   |
| `saved_messages_title`               | `{count} Saved Messages`                                          |
| `pinned_by_system`                   | Pinned by the app                                                 |
| `conversation_pin`                   | Pin conversation                                                  |
| `conversation_unpin`                 | Unpin conversation                                                |
| `conversation_pinned_toast`          | Conversation pinned                                               |
| `conversation_unpinned_toast`        | Conversation unpinned                                             |
| `pin_message_limit_error`            | `You can only pin {limit} messages. Unpin one to pin another.`    |
| `save_message_limit_error`           | `You can only save {limit} messages. Unsave one to save another.` |
| `conversation_pin_limit_error`       | `You can only pin {limit} chats. Unpin one to pin another.`       |
| `pin_message_permission_error`       | You don't have permission to pin messages here.                   |
| `pin_save_generic_error`             | Something went wrong. Please try again.                           |

Override any of these through [Localization](/docs/ui-kit/angular/customization/localization).

***

## Related

* [CometChatPinnedMessages](/docs/ui-kit/angular/components/cometchat-pinned-messages) — the conversation's pinned list
* [CometChatSavedMessages](/docs/ui-kit/angular/components/cometchat-saved-messages) — the user's saved list
* [CometChatMessageList](/docs/ui-kit/angular/components/cometchat-message-list) — where the actions live
* [CometChatConversations](/docs/ui-kit/angular/components/cometchat-conversations) — pinning a conversation
* [Events](/docs/ui-kit/angular/events#cometchatpinsaveevents) — the `CometChatPinSaveEvents` reference
* [Global Configuration](/docs/ui-kit/angular/customization/global-config) — the development overrides
