diff --git a/docs.json b/docs.json
index cb43da08b..86ba0da3d 100644
--- a/docs.json
+++ b/docs.json
@@ -4589,7 +4589,10 @@
"sdk/flutter/edit-message",
"sdk/flutter/flag-message",
"sdk/flutter/delete-message",
+ "sdk/flutter/pin-messages",
+ "sdk/flutter/save-messages",
"sdk/flutter/delete-conversation",
+ "sdk/flutter/pin-conversations",
"sdk/flutter/typing-indicators",
"sdk/flutter/transient-messages",
"sdk/flutter/delivery-read-receipts",
diff --git a/sdk/flutter/pin-conversations.mdx b/sdk/flutter/pin-conversations.mdx
new file mode 100644
index 000000000..fb6218eec
--- /dev/null
+++ b/sdk/flutter/pin-conversations.mdx
@@ -0,0 +1,106 @@
+---
+title: "Pin Conversations"
+description: "Pin CometChat conversations to the top of the list in Flutter apps and keep every device in sync through real-time pin events."
+---
+
+
+
+Pinning a conversation surfaces it at the top of the logged-in user's conversation list. Conversation pins are **per-user** — pinning a conversation does not affect how the other participants see their lists. A pinned conversation carries a `pinnedAt` timestamp and the `pinnedBy` uid.
+
+A conversation can also be pinned for the user by an admin surface, in which case `pinnedBy` carries the `app_system` sentinel. System pins rank above the user's own pins and cannot be removed from the client.
+
+## Pin a Conversation
+
+In order to pin a conversation, you can use the `pinConversation()` method. This method takes the uid/guid of the conversation counterpart and the conversation type (`user`/`group`). On success it returns the full updated `Conversation` with `pinnedAt` and `pinnedBy` stamped.
+
+
+
+```dart
+String conversationWith = "cometchat-uid-1";
+String conversationType = CometChatConversationType.user;
+
+CometChat.pinConversation(conversationWith, conversationType,
+ onSuccess: (Conversation conversation) {
+ debugPrint("Conversation pinned at: ${conversation.pinnedAt}");
+ }, onError: (CometChatException e) {
+ debugPrint("Conversation pinning failed with exception: ${e.message}");
+});
+```
+
+
+
+
+The call is idempotent — pinning an already-pinned conversation succeeds and returns the current state.
+
+## Unpin a Conversation
+
+In order to unpin a conversation, you can use the `unpinConversation()` method. Only a pin placed by the logged-in user can be removed — an `app_system` pin is rejected server-side. The returned `Conversation` carries the pin fields cleared to `null`.
+
+
+
+```dart
+String conversationWith = "cometchat-uid-1";
+String conversationType = CometChatConversationType.user;
+
+CometChat.unpinConversation(conversationWith, conversationType,
+ onSuccess: (Conversation conversation) {
+ debugPrint("Conversation unpinned");
+ }, onError: (CometChatException e) {
+ debugPrint("Conversation unpinning failed with exception: ${e.message}");
+});
+```
+
+
+
+
+## Real-Time Pin Events
+
+Pin and unpin events are delivered to the logged-in user's devices through the `ConversationListener` class — the acting device receives the callback on success, and the user's other devices receive it over the socket, so lists stay in sync everywhere. Admin (`app_system`) pins applied server-side arrive through the same callbacks.
+
+To receive them, register a listener using the `addConversationListener()` method and override the `onConversationPinned()` and `onConversationUnpinned()` callbacks. Remove the listener with `removeConversationListener()` when it is no longer needed.
+
+
+
+```dart
+class Class_Name with ConversationListener {
+
+ //CometChat.addConversationListener("listenerId", this);
+
+ @override
+ void onConversationPinned(Conversation conversation) {
+ debugPrint("Conversation pinned: ${conversation.conversationId}");
+ }
+
+ @override
+ void onConversationUnpinned(Conversation conversation) {
+ debugPrint("Conversation unpinned: ${conversation.conversationId}");
+ }
+}
+```
+
+
+
+
+When applying these events to a conversation list, keep the ordering contract: system pins (`pinnedBy == "app_system"`) stay above user pins, and user pins stay above the activity-ordered rest of the list.
+
+## Fetching and Ordering
+
+Pinned conversations are returned by the regular `ConversationsRequest` described in [Retrieve Conversations](/sdk/flutter/retrieve-conversations), ordered pinned-first — system pins, then the user's pins, then the remaining conversations by latest activity. Inspect `conversation.pinnedAt` / `conversation.pinnedBy` on the fetched objects to render the pinned state.
+
+## Feature Availability and Limits
+
+Whether the Pin Conversation feature is enabled for the logged-in user is served on the user's login payload. You can check it at any time using the synchronous `isPinConversationEnabled()` method — it never throws, and returns `true` when the backend did not serve the flag.
+
+The maximum number of conversations a user can pin is available through `getPinnedConversationsLimit()`, which returns `null` when the backend did not serve a limit. When a pin call exceeds the cap, it fails with a limit-exceeded error whose `errorParams` map carries the authoritative limit as `{"limit": n}`.
+
+
+
+```dart
+if (CometChat.isPinConversationEnabled()) {
+ int? limit = CometChat.getPinnedConversationsLimit();
+ debugPrint("Conversation pinning enabled, limit: ${limit ?? "server default"}");
+}
+```
+
+
+
diff --git a/sdk/flutter/pin-messages.mdx b/sdk/flutter/pin-messages.mdx
new file mode 100644
index 000000000..e6112b4fd
--- /dev/null
+++ b/sdk/flutter/pin-messages.mdx
@@ -0,0 +1,146 @@
+---
+title: "Pin Messages"
+description: "Pin and unpin CometChat messages in Flutter apps, listen to pin events in real time, and fetch the pinned messages of a conversation."
+---
+
+
+
+Pinning highlights an important message for **everyone in the conversation**. A pinned message carries a `pinnedAt` timestamp and the `pinnedBy` uid of the member who pinned it, and every participant can fetch the conversation's pinned list.
+
+Pinning is permissioned in groups — only participants with the admin, moderator or owner scope can pin or unpin. In one-to-one conversations both participants can.
+
+## Pin a Message
+
+*In other words, as a member of a conversation, how do I pin a message for everyone?*
+
+In order to pin a message, you can use the `pinMessage()` method. This method takes the id of the message to be pinned. On success it returns the **full updated message** with `pinnedAt` and `pinnedBy` stamped.
+
+
+
+```dart
+int messageId = 103;
+
+CometChat.pinMessage(messageId, onSuccess: (BaseMessage message) {
+ debugPrint("Message pinned successfully: ${message.pinnedAt}");
+}, onError: (CometChatException e) {
+ debugPrint("Message pinning failed with exception: ${e.message}");
+});
+```
+
+
+
+
+The call is idempotent — pinning an already-pinned message succeeds and returns the current state.
+
+## Unpin a Message
+
+In order to unpin a message, you can use the `unpinMessage()` method. The returned message carries the pin fields cleared to `null`. The same permission model applies.
+
+
+
+```dart
+int messageId = 103;
+
+CometChat.unpinMessage(messageId, onSuccess: (BaseMessage message) {
+ debugPrint("Message unpinned successfully");
+}, onError: (CometChatException e) {
+ debugPrint("Message unpinning failed with exception: ${e.message}");
+});
+```
+
+
+
+
+## Real-Time Pin Events
+
+Pin and unpin actions are delivered to all participants through the `MessageListener` class. To receive them, register a listener using the `addMessageListener()` method and override the `onMessagePinned()` and `onMessageUnpinned()` callbacks. Both receive the full updated message object.
+
+
+
+```dart
+class Class_Name with MessageListener {
+
+ //CometChat.addMessageListener("listenerId", this);
+
+ @override
+ void onMessagePinned(BaseMessage message) {
+ debugPrint("Message pinned: ${message.id} by ${message.pinnedBy}");
+ }
+
+ @override
+ void onMessageUnpinned(BaseMessage message) {
+ debugPrint("Message unpinned: ${message.id}");
+ }
+}
+```
+
+
+
+
+The device that performed the action also receives these callbacks on success, so a single code path can update your UI for your own pins and for pins made by other members or your other devices.
+
+## Fetch Pinned Messages
+
+You can fetch all the pinned messages of a conversation by using the `MessagesRequest` class with the `pinned` parameter of the `MessagesRequestBuilder` set to `true`. A pinned list belongs to one conversation, so pair it with the `uid` (for a user conversation) or `guid` (for a group).
+
+
+
+```dart
+String UID = "cometchat-uid-1";
+
+MessagesRequest messageRequest = (MessagesRequestBuilder()
+ ..uid = UID
+ ..pinned = true
+ ..limit = 50).build();
+
+messageRequest.fetchPrevious(onSuccess: (List list) {
+ debugPrint("Pinned messages fetched: ${list.length}");
+}, onError: (CometChatException e) {
+ debugPrint("Pinned message fetching failed with exception: ${e.message}");
+});
+```
+
+
+
+
+## Feature Availability and Limits
+
+Whether the Pin Message feature is enabled for the logged-in user is served on the user's login payload. You can check it at any time using the synchronous `isPinMessageEnabled()` method — it never throws, and returns `true` when the backend did not serve the flag so the feature is not disabled on older backends.
+
+The maximum number of messages that can be pinned per conversation is also served on the login payload and is available through `getPinnedMessagesLimit()`. It returns `null` when the backend did not serve a limit.
+
+
+
+```dart
+if (CometChat.isPinMessageEnabled()) {
+ int? limit = CometChat.getPinnedMessagesLimit();
+ debugPrint("Pinning enabled, limit: ${limit ?? "server default"}");
+}
+```
+
+
+
+
+When a pin call exceeds the cap, it fails with the `ERR_PINNED_MESSAGES_LIMIT_EXCEEDED` error code. The exception's `errorParams` map carries the authoritative limit as `{"limit": n}`, which you can interpolate into your error copy.
+
+
+
+```dart
+CometChat.pinMessage(messageId, onSuccess: (BaseMessage message) {
+ debugPrint("Message pinned");
+}, onError: (CometChatException e) {
+ if (e.code == 'ERR_PINNED_MESSAGES_LIMIT_EXCEEDED') {
+ final limit = e.errorParams?['limit'];
+ debugPrint("You can only pin $limit messages. Unpin one to pin another.");
+ }
+});
+```
+
+
+
+
+
+
+Pins placed from an admin surface carry the `app_system` sentinel in `pinnedBy`. Save is the private, per-user counterpart of pinning — see [Save Messages](/sdk/flutter/save-messages).
+
+
diff --git a/sdk/flutter/save-messages.mdx b/sdk/flutter/save-messages.mdx
new file mode 100644
index 000000000..5666a6bf0
--- /dev/null
+++ b/sdk/flutter/save-messages.mdx
@@ -0,0 +1,119 @@
+---
+title: "Save Messages"
+description: "Save (bookmark) CometChat messages privately in Flutter apps, sync saves across devices, and fetch the logged-in user's saved messages."
+---
+
+
+
+Saving bookmarks a message **privately for the logged-in user**. Unlike [pinning](/sdk/flutter/pin-messages), a save is per-viewer: no other member is notified, nothing changes for the rest of the conversation, and any message the user can read — their own or someone else's — can be saved. A saved message carries a `savedAt` timestamp visible only to the user who saved it.
+
+## Save a Message
+
+In order to save a message, you can use the `saveMessage()` method. This method takes the id of the message to be saved. On success it returns the full updated message with `savedAt` stamped.
+
+
+
+```dart
+int messageId = 103;
+
+CometChat.saveMessage(messageId, onSuccess: (BaseMessage message) {
+ debugPrint("Message saved successfully: ${message.savedAt}");
+}, onError: (CometChatException e) {
+ debugPrint("Message saving failed with exception: ${e.message}");
+});
+```
+
+
+
+
+The call is idempotent — saving an already-saved message succeeds and returns the current state.
+
+## Unsave a Message
+
+In order to unsave a message, you can use the `unsaveMessage()` method. The returned message carries `savedAt` cleared to `null`.
+
+
+
+```dart
+int messageId = 103;
+
+CometChat.unsaveMessage(messageId, onSuccess: (BaseMessage message) {
+ debugPrint("Message unsaved successfully");
+}, onError: (CometChatException e) {
+ debugPrint("Message unsaving failed with exception: ${e.message}");
+});
+```
+
+
+
+
+## Real-Time Save Events
+
+Because saves are private, save events are delivered only to the **logged-in user's own devices** — the acting device receives the callback on success, and the user's other devices receive it over the socket for cross-device sync. Register a `MessageListener` using the `addMessageListener()` method and override the `onMessageSaved()` and `onMessageUnsaved()` callbacks.
+
+
+
+```dart
+class Class_Name with MessageListener {
+
+ //CometChat.addMessageListener("listenerId", this);
+
+ @override
+ void onMessageSaved(BaseMessage message) {
+ debugPrint("Message saved: ${message.id}");
+ }
+
+ @override
+ void onMessageUnsaved(BaseMessage message) {
+ debugPrint("Message unsaved: ${message.id}");
+ }
+}
+```
+
+
+
+
+## Fetch Saved Messages
+
+You can fetch the logged-in user's saved messages by using the `MessagesRequest` class with the `saved` parameter of the `MessagesRequestBuilder` set to `true`.
+
+
+
+```dart
+MessagesRequest messageRequest = (MessagesRequestBuilder()
+ ..saved = true
+ ..limit = 50).build();
+
+messageRequest.fetchPrevious(onSuccess: (List list) {
+ debugPrint("Saved messages fetched: ${list.length}");
+}, onError: (CometChatException e) {
+ debugPrint("Saved message fetching failed with exception: ${e.message}");
+});
+```
+
+
+
+
+## Feature Availability and Limits
+
+Whether the Save Message feature is enabled for the logged-in user is served on the user's login payload. You can check it at any time using the synchronous `isSaveMessageEnabled()` method — it never throws, and returns `true` when the backend did not serve the flag.
+
+The maximum number of messages a user can save is available through `getSavedMessagesLimit()`, which returns `null` when the backend did not serve a limit.
+
+When a save call exceeds the cap, it fails with the `ERR_SAVED_MESSAGES_LIMIT_EXCEEDED` error code. The exception's `errorParams` map carries the authoritative limit as `{"limit": n}`.
+
+
+
+```dart
+CometChat.saveMessage(messageId, onSuccess: (BaseMessage message) {
+ debugPrint("Message saved");
+}, onError: (CometChatException e) {
+ if (e.code == 'ERR_SAVED_MESSAGES_LIMIT_EXCEEDED') {
+ final limit = e.errorParams?['limit'];
+ debugPrint("You can only save $limit messages. Unsave one to save another.");
+ }
+});
+```
+
+
+
diff --git a/sdk/flutter/threaded-messages.mdx b/sdk/flutter/threaded-messages.mdx
index c796bcd1b..4b2cd64cb 100644
--- a/sdk/flutter/threaded-messages.mdx
+++ b/sdk/flutter/threaded-messages.mdx
@@ -149,3 +149,117 @@ messageRequest.fetchNext(onSuccess: (List list) {
The above snippet will return messages between the logged in user and `cometchat-uid-1` excluding all the threaded messages belonging to the same conversation.
+
+## Thread Subscriptions
+
+Subscribing to (following) a thread lets the logged-in user be notified about new replies in that thread. Subscriptions are automatic where it matters — replying to a thread or being @-mentioned in one subscribes the user — and can also be toggled explicitly.
+
+### Subscribe to a Thread
+
+In order to subscribe to a thread, you can use the `subscribeToThread()` method. This method takes the id of the thread's parent message. Subscribing is allowed even on a message that has no replies yet.
+
+
+
+```dart
+int parentMessageId = 103;
+
+CometChat.subscribeToThread(parentMessageId, onSuccess: (String response) {
+ debugPrint("Subscribed to thread successfully");
+}, onError: (CometChatException e) {
+ debugPrint("Thread subscription failed with exception: ${e.message}");
+});
+```
+
+
+
+
+The call is idempotent — subscribing to an already-followed thread succeeds and is not an error.
+
+### Unsubscribe from a Thread
+
+In order to unsubscribe from a thread, you can use the `unsubscribeFromThread()` method.
+
+
+
+```dart
+int parentMessageId = 103;
+
+CometChat.unsubscribeFromThread(parentMessageId, onSuccess: (String response) {
+ debugPrint("Unsubscribed from thread successfully");
+}, onError: (CometChatException e) {
+ debugPrint("Thread unsubscription failed with exception: ${e.message}");
+});
+```
+
+
+
+
+
+
+Unfollowing is not sticky: replying to the thread again, or being @-mentioned in it, re-subscribes the user.
+
+
+
+### Check the Subscription State
+
+You can read the logged-in user's cached subscription state for any thread using the synchronous `getThreadSubscriptionState()` method. It is safe to call at any time — before login or on an empty cache it returns `ThreadSubscriptionState.unknown` and never throws. Render `unknown` as the un-followed affordance; an unnecessary subscribe is harmless because the endpoint is idempotent.
+
+
+
+```dart
+ThreadSubscriptionState state = CometChat.getThreadSubscriptionState(103);
+
+if (state == ThreadSubscriptionState.subscribed) {
+ debugPrint("Following this thread");
+}
+```
+
+
+
+
+When fetching a single message with `getMessageDetails()`, the returned message's `threadSubscribed` field also reflects the logged-in user's subscription state for its thread.
+
+### Real-Time Subscription Events
+
+Subscription changes are delivered through the `ThreadListener` class. Register it using the `addThreadListener()` method and remove it with `removeThreadListener()` in `dispose()`. The `onThreadSubscriptionChanged()` callback receives a `ThreadSubscriptionEvent` carrying the `parentMessageId` and the new `subscriptionState`.
+
+
+
+```dart
+class Class_Name with ThreadListener {
+
+ //CometChat.addThreadListener("listenerId", this);
+
+ @override
+ void onThreadSubscriptionChanged(ThreadSubscriptionEvent event) {
+ debugPrint("Thread ${event.parentMessageId} is now ${event.subscriptionState}");
+ }
+}
+```
+
+
+
+
+### Fetch Participated Threads
+
+You can fetch the threads the logged-in user participates in by using the `ThreadsRequest` class. The `ThreadsRequestBuilder` builds the request using functions such as `setLimit()` and `setParticipatedByMe()`; once you have the `ThreadsRequest` object, call `fetchNext()` to get the next set of threads.
+
+
+
+```dart
+ThreadsRequest threadsRequest = (ThreadsRequestBuilder()
+ ..setParticipatedByMe(true)
+ ..setLimit(30)).build();
+
+List threads = await threadsRequest.fetchNext();
+debugPrint("Fetched ${threads.length} threads");
+```
+
+
+
+
+
+
+Unsubscribing removes the thread from the participated-threads list server-side — if you are holding a fetched list, remove the row locally as well.
+
+
diff --git a/ui-kit/flutter/conversations.mdx b/ui-kit/flutter/conversations.mdx
index bda98e431..fee7f0dbf 100644
--- a/ui-kit/flutter/conversations.mdx
+++ b/ui-kit/flutter/conversations.mdx
@@ -263,6 +263,7 @@ The component listens to these SDK events internally. No manual setup needed.
| `hideSearch` | `bool?` | `null` | Toggle search bar |
| `searchReadOnly` | `bool` | `false` | Make search bar read-only (tap opens custom search) |
| `deleteConversationOptionVisibility` | `bool?` | `true` | Show delete option on long press |
+| `pinConversationOptionVisibility` | `bool?` | `true` | Show pin/unpin option on long press |
| `groupTypeVisibility` | `bool?` | `true` | Show group type icon on avatar |
| `usersStatusVisibility` | `bool?` | `true` | Show online/offline status indicator |
| `receiptsVisibility` | `bool?` | `true` | Show message receipts |
@@ -428,6 +429,26 @@ CometChatConversations(
---
+## Pin Conversations
+
+The long-press menu offers **Pin conversation** / **Unpin conversation** alongside Delete. Pinned conversations form a shelf at the top of the list, ordered system pins first (placed by an admin surface, `pinnedBy == "app_system"`), then the user's own pins, then the remaining conversations by latest activity — and real-time events keep that ordering: new activity floats a conversation to the top of **its own section only**, never above a pin.
+
+Everything is wired internally through the SDK's [Pin Conversations](/sdk/flutter/pin-conversations) APIs: the component performs the pin/unpin call, shows a confirmation toast (or the cap error with the server's limit interpolated), and reorders the list from the SDK's `ConversationListener` events — including pins made from the user's other devices.
+
+To hide the option, set `pinConversationOptionVisibility` to `false`. The option also hides itself when the feature is disabled for the logged-in user (`CometChat.isPinConversationEnabled()`), and for rows pinned by `app_system`, which are not user-removable.
+
+
+
+```dart
+CometChatConversations(
+ pinConversationOptionVisibility: false, // hide pin/unpin from the long-press menu
+)
+```
+
+
+
+---
+
## Common Patterns
### Minimal list — hide all chrome
diff --git a/ui-kit/flutter/guide-threaded-messages.mdx b/ui-kit/flutter/guide-threaded-messages.mdx
index d63735c6d..b5ba44934 100644
--- a/ui-kit/flutter/guide-threaded-messages.mdx
+++ b/ui-kit/flutter/guide-threaded-messages.mdx
@@ -102,6 +102,31 @@ CometChatMessageComposer(
+## Thread Subscriptions (Follow / Unfollow)
+
+Users can follow a thread to be notified about new replies, and unfollow to mute it. The UI Kit wires this end to end on top of the SDK's [Thread Subscriptions](/sdk/flutter/threaded-messages#thread-subscriptions) APIs — subscriptions also happen automatically when the user replies to a thread or is @-mentioned in one.
+
+Enable the feature through `UIKitSettings` when initializing the UI Kit:
+
+
+
+```dart
+UIKitSettings uiKitSettings = (UIKitSettingsBuilder()
+ ..appId = "APP_ID"
+ ..region = "REGION"
+ ..authKey = "AUTH_KEY"
+ ..enableThreadSubscription = true
+).build();
+```
+
+
+
+With the gate on:
+
+- The message action menu offers **Subscribe to thread** / **Unsubscribe from thread** on threaded messages, with confirmation toasts ("Subscribed. You'll be notified about new replies in this thread.").
+- The thread screen's header shows a **notification bell** reflecting the live subscription state — tapping it toggles the subscription. Pass the thread's root message as `parentMessage` to `CometChatMessageHeader` to render it; on `CometChatThreadedHeader`, the bell can be hidden with `threadSubscriptionVisibility: false` when the header above it already carries one.
+- All copy is localized through the UI Kit's translations.
+
## Customization Options
- Header Styling: Customize `CometChatThreadedHeader` appearance
diff --git a/ui-kit/flutter/message-composer.mdx b/ui-kit/flutter/message-composer.mdx
index 9862351e8..6879f2fe1 100644
--- a/ui-kit/flutter/message-composer.mdx
+++ b/ui-kit/flutter/message-composer.mdx
@@ -459,6 +459,40 @@ CometChatMessageComposer(
+### Trailing toolbar actions
+
+`richTextToolbarActions` appends your own buttons at the trailing end of the built-in toolbar — after the format buttons, separated by a divider — without replacing the toolbar the way `richTextToolbarView` does. It takes the same `ComposerActionsBuilder` shape as `attachmentOptions` and returns a list of `CometChatMessageComposerAction` items.
+
+For toolbar actions, set the action's `onToolbarTap` callback. It receives the `BuildContext` and the composer's active `TextEditingController`, so the action can read the current text and selection and mutate the draft — the composer stays the owner of the field.
+
+
+
+```dart
+CometChatMessageComposer(
+ user: user,
+ richTextToolbarActions: (context, user, group, id) => [
+ CometChatMessageComposerAction(
+ id: 'insert_greeting',
+ title: 'Greeting',
+ icon: const Icon(Icons.waving_hand_outlined),
+ onToolbarTap: (context, controller) {
+ final selection = controller.selection;
+ final offset = selection.isValid ? selection.start : controller.text.length;
+ controller.text = controller.text.replaceRange(offset, offset, 'Hello! ');
+ },
+ ),
+ ],
+)
+```
+
+
+
+
+
+On web, tapping a toolbar button can blur the text field and collapse the selection before your handler runs. When the controller is a `RichTextEditingController`, fall back to its `lastNonCollapsedSelection` — the most recent valid selection the field held — and bounds-check it against the current text.
+
+
+
***
## Multiple Attachments
diff --git a/ui-kit/flutter/message-list.mdx b/ui-kit/flutter/message-list.mdx
index 449301f06..589a79a91 100644
--- a/ui-kit/flutter/message-list.mdx
+++ b/ui-kit/flutter/message-list.mdx
@@ -204,6 +204,8 @@ The component listens to SDK message events internally. No manual setup needed.
| `onMessagesDelivered` / `onMessagesRead` | Updates receipt status via ValueNotifier |
| `onTypingStarted` / `onTypingEnded` | Updates typing indicator |
| `onMessageReactionAdded` / `onMessageReactionRemoved` | Updates reaction counts |
+| `onMessagePinned` / `onMessageUnpinned` | Restamps the message's pin state in-place |
+| `onMessageSaved` / `onMessageUnsaved` | Restamps the message's saved state (own devices only) |
| Connection reconnected | Triggers silent sync to fetch missed messages |
---
@@ -450,6 +452,8 @@ To stage and send multiple attachments, see [Message Composer](/ui-kit/flutter/m
| `hideDeleteMessageOption` | `false` | Hide "Delete Message" |
| `hideEditMessageOption` | `false` | Hide "Edit Message" |
| `hideMessageInfoOption` | `false` | Hide "Message Info" |
+| `hidePinMessageOption` | `false` | Hide "Pin" / "Unpin" |
+| `hideSaveMessageOption` | `false` | Hide "Save" / "Unsave" |
| `hideMessagePrivatelyOption` | `false` | Hide "Message Privately" |
| `hideReactionOption` | `false` | Hide "Reaction" |
| `hideReplyInThreadOption` | `false` | Hide "Reply in Thread" |
@@ -544,6 +548,24 @@ CometChatMessageList(
+### Pin or save a message
+
+The action menu offers **Pin** / **Unpin** and **Save** / **Unsave** on every message, wired internally to the SDK's [Pin Messages](/sdk/flutter/pin-messages) and [Save Messages](/sdk/flutter/save-messages) APIs. No setup is needed: the component performs the call, shows a confirmation toast, updates the bubble through the SDK's listener fan-out (so pins made by other members — and saves made on your other devices — land too), and maps the cap errors (`ERR_PINNED_MESSAGES_LIMIT_EXCEEDED` / `ERR_SAVED_MESSAGES_LIMIT_EXCEEDED`) to localized error copy with the server's limit interpolated.
+
+Pin respects the conversation's permission model (group pinning requires admin/moderator/owner scope), and both options hide themselves when the feature is disabled for the logged-in user. To hide them regardless, use `hidePinMessageOption` / `hideSaveMessageOption`.
+
+
+
+```dart
+CometChatMessageList(
+ user: user,
+ hidePinMessageOption: true,
+ hideSaveMessageOption: true,
+)
+```
+
+
+
### Mark a message as unread
Expose the "Mark as Unread" option in the long-press menu: