From 2e93ebdcac2d36e504b5b1e5cf22b5bb8dd2753f Mon Sep 17 00:00:00 2001 From: Anshuman-cc Date: Fri, 7 Aug 2026 17:16:09 +0530 Subject: [PATCH] docs(flutter): thread subscriptions, pin & save, composer trailing toolbar actions - New SDK v5 pages: Pin Messages, Save Messages, Pin Conversations (events, fetching, feature flags, limits and cap error handling) - Thread Subscriptions section on the SDK Threaded Messages page (subscribe/unsubscribe, cached state, ThreadListener, ThreadsRequest) - UI Kit v6: trailing toolbar actions on Message Composer (richTextToolbarActions + onToolbarTap), Pin Conversations section and option visibility on Conversations, pin/save options + events on Message List, thread-subscription gate and bell on the Threaded Messages guide - Register the three new SDK pages in the Messaging nav group Co-Authored-By: Claude Fable 5 --- docs.json | 3 + sdk/flutter/pin-conversations.mdx | 106 +++++++++++++++ sdk/flutter/pin-messages.mdx | 146 +++++++++++++++++++++ sdk/flutter/save-messages.mdx | 119 +++++++++++++++++ sdk/flutter/threaded-messages.mdx | 114 ++++++++++++++++ ui-kit/flutter/conversations.mdx | 21 +++ ui-kit/flutter/guide-threaded-messages.mdx | 25 ++++ ui-kit/flutter/message-composer.mdx | 34 +++++ ui-kit/flutter/message-list.mdx | 22 ++++ 9 files changed, 590 insertions(+) create mode 100644 sdk/flutter/pin-conversations.mdx create mode 100644 sdk/flutter/pin-messages.mdx create mode 100644 sdk/flutter/save-messages.mdx 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: