Skip to main content
The UI Kit ships three related but distinct actions: 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.

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

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

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:
See CometChatPinnedMessages.

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.
See CometChatSavedMessages.

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.

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

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

Override any of these through Localization.