From 2e93ebdcac2d36e504b5b1e5cf22b5bb8dd2753f Mon Sep 17 00:00:00 2001 From: Anshuman-cc Date: Fri, 7 Aug 2026 17:16:09 +0530 Subject: [PATCH 1/8] 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: From 9c9b5b96f82d503d4515dd0fc5efff223976b2d7 Mon Sep 17 00:00:00 2001 From: Anshuman-cc Date: Wed, 12 Aug 2026 20:07:42 +0530 Subject: [PATCH 2/8] docs(flutter): rewrite Thread Subscriptions for the stateless SDK redesign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Subscription state now lives on the message (threadSubscribed boolean, served on every fetch), the resolved subscribe/unsubscribe call is the acknowledgement, and replies arrive on the standard MessageListener — no ThreadListener, no getThreadSubscriptionState, no SDK-side events. Documents the two consumer rules: an un-asked false means "the server did not tell me", and a fetched false on the author's own message is an explicit unsubscribe. Co-Authored-By: Claude Fable 5 --- sdk/flutter/threaded-messages.mdx | 45 ++++++++++++++++--------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/sdk/flutter/threaded-messages.mdx b/sdk/flutter/threaded-messages.mdx index 4b2cd64cb..060421064 100644 --- a/sdk/flutter/threaded-messages.mdx +++ b/sdk/flutter/threaded-messages.mdx @@ -152,7 +152,7 @@ The above snippet will return messages between the logged in user and `cometchat ## 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. +Subscribing to (following) a thread lets the logged-in user be notified about new replies in that thread. The SDK exposes the server's answer on the message and two calls to change it — subscription state lives on the message, not in the SDK. ### Subscribe to a Thread @@ -173,7 +173,7 @@ CometChat.subscribeToThread(parentMessageId, onSuccess: (String response) { -The call is idempotent — subscribing to an already-followed thread succeeds and is not an error. +The call is idempotent — subscribing to an already-followed thread succeeds and is not an error. **The resolved call is the acknowledgement**: there is no follow-up event. ### Unsubscribe from a Thread @@ -196,20 +196,18 @@ CometChat.unsubscribeFromThread(parentMessageId, onSuccess: (String response) { -Unfollowing is not sticky: replying to the thread again, or being @-mentioned in it, re-subscribes the user. +Unfollowing is not sticky: replying to the thread again, or being @-mentioned in it, re-subscribes the user. Do not promise otherwise in UI copy. Unsubscribing also hard-deletes the subscription server-side — if you are holding a fetched thread list, remove the row locally when the call resolves. -### Check the Subscription State +### Read 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. +Subscription state lives **on the message**. Every message fetch opts in with `withThreadSubscribed=true` automatically, and the server returns the flag on the message payload — the SDK normalises it and exposes it as the `threadSubscribed` boolean on any fetched message. The flag is per-viewer: the same message yields different values for different users. ```dart -ThreadSubscriptionState state = CometChat.getThreadSubscriptionState(103); - -if (state == ThreadSubscriptionState.subscribed) { +if (message.threadSubscribed) { debugPrint("Following this thread"); } ``` @@ -217,22 +215,29 @@ if (state == ThreadSubscriptionState.subscribed) { -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. +Two rules to internalise: + +1. The flag is only populated on responses to requests that **asked** for it. A message delivered over the socket carries no flag and therefore reads `false` — meaning "the server did not tell me", not "the user is unsubscribed". +2. **A message's author is subscribed to its thread by default.** A fetched message the logged-in user sent reading `false` is therefore not silence — it is an explicit unsubscribe, and nothing should override it. + +Because subscribing is idempotent, rendering an un-told `false` as the un-followed affordance is safe — an unnecessary subscribe is harmless. -### Real-Time Subscription Events +### Reacting to Replies -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`. +A thread reply is an ordinary message with `parentMessageId` set, delivered through the standard `MessageListener` alongside every other message — there is no thread-specific event channel. The same listener also delivers `onMessageEdited` and `onMessageDeleted` for replies, which a replies-only channel would miss. ```dart -class Class_Name with ThreadListener { +class Class_Name with MessageListener { - //CometChat.addThreadListener("listenerId", this); + //CometChat.addMessageListener("listenerId", this); @override - void onThreadSubscriptionChanged(ThreadSubscriptionEvent event) { - debugPrint("Thread ${event.parentMessageId} is now ${event.subscriptionState}"); + void onTextMessageReceived(TextMessage textMessage) { + if (textMessage.parentMessageId != 0) { + debugPrint("A reply landed in thread ${textMessage.parentMessageId}"); + } } } ``` @@ -240,9 +245,11 @@ class Class_Name with ThreadListener { +Your own replies do not arrive on a listener — update your UI from the send call's success callback. + ### 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. +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. Presence in this list *is* a subscription — every row is subscribed. @@ -257,9 +264,3 @@ 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. - - From 13f28a552e990896ac8006a3d73daeb3773b32a2 Mon Sep 17 00:00:00 2001 From: Anshuman-cc Date: Mon, 7 Sep 2026 19:53:41 +0530 Subject: [PATCH 3/8] docs(flutter): pin/save hide flags are per-state, not per-feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The option table and example implied one flag per feature. The kit has four: the menu renders Pin or Unpin (Save or Unsave) depending on the message, and each rendered state has its own flag — message_template_utils picks hideUnpinMessageOption when the message is pinned and hidePinMessageOption when it is not. As written, hidePinMessageOption: true still left "Unpin" on the menu for already-pinned messages, which is the opposite of what the example suggests it does. Co-Authored-By: Claude Opus 5 --- ui-kit/flutter/message-list.mdx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/ui-kit/flutter/message-list.mdx b/ui-kit/flutter/message-list.mdx index 589a79a91..d3f6b090a 100644 --- a/ui-kit/flutter/message-list.mdx +++ b/ui-kit/flutter/message-list.mdx @@ -452,8 +452,10 @@ 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" | +| `hidePinMessageOption` | `false` | Hide "Pin" (shown while the message is unpinned) | +| `hideUnpinMessageOption` | `false` | Hide "Unpin" (shown while the message is pinned) | +| `hideSaveMessageOption` | `false` | Hide "Save" (shown while the message is unsaved) | +| `hideUnsaveMessageOption` | `false` | Hide "Unsave" (shown while the message is saved) | | `hideMessagePrivatelyOption` | `false` | Hide "Message Privately" | | `hideReactionOption` | `false` | Hide "Reaction" | | `hideReplyInThreadOption` | `false` | Hide "Reply in Thread" | @@ -552,15 +554,20 @@ CometChatMessageList( 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`. +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, note that each state has its own flag: the menu shows **Pin** or **Unpin** depending on the message, so hiding pinning entirely takes `hidePinMessageOption` *and* `hideUnpinMessageOption`. Save works the same way. ```dart CometChatMessageList( user: user, + // both flags per feature — one per rendered state hidePinMessageOption: true, + hideUnpinMessageOption: true, hideSaveMessageOption: true, + hideUnsaveMessageOption: true, ) ``` From ef615b474fe99020cfeb0be09eca19868feed88e Mon Sep 17 00:00:00 2001 From: Anshuman-cc Date: Mon, 7 Sep 2026 20:00:38 +0530 Subject: [PATCH 4/8] docs(flutter): cover the 6.1.1 pin/save and thread API the PR was missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New pages for CometChatPinnedMessages and CometChatSavedMessages — both are publicly exported in 6.1.1 and had no documentation at all — added to the Components nav. Also documents, against the shipped source rather than the design docs: - The five new Kit events (ccMessagePinned/Unpinned/Saved/Unsaved and ccThreadSubscriptionChanged) in events.mdx, noting they are default no-ops so existing listeners keep compiling. - CometChatMessageListController.jumpToMessage in message-list.mdx, with a warning that v5 had a different class of the same name which the upgrade guide maps to MessageListBloc. - hideThreadSubscriptionOption in the message-list option table. - The header's pinned-messages entry point and thread-subscription bell. Documented as an item in the overflow menu, which is where _overflowEntries actually puts it — the prop's own doc comment still says "pin icon in the trailing area" and is stale. - onThreadSubscriptionChange and threadSubscriptionVisibility on CometChatThreadedHeader. Every internal link resolves and docs.json still parses. Co-Authored-By: Claude Opus 5 --- docs.json | 2 + ui-kit/flutter/events.mdx | 12 +++ ui-kit/flutter/message-header.mdx | 29 +++++++ ui-kit/flutter/message-list.mdx | 31 +++++++ ui-kit/flutter/pinned-messages.mdx | 96 +++++++++++++++++++++ ui-kit/flutter/saved-messages.mdx | 92 ++++++++++++++++++++ ui-kit/flutter/threaded-messages-header.mdx | 22 +++++ 7 files changed, 284 insertions(+) create mode 100644 ui-kit/flutter/pinned-messages.mdx create mode 100644 ui-kit/flutter/saved-messages.mdx diff --git a/docs.json b/docs.json index 86ba0da3d..34e0566d3 100644 --- a/docs.json +++ b/docs.json @@ -2263,6 +2263,8 @@ "ui-kit/flutter/message-list", "ui-kit/flutter/message-composer", "ui-kit/flutter/threaded-messages-header", + "ui-kit/flutter/pinned-messages", + "ui-kit/flutter/saved-messages", "ui-kit/flutter/incoming-call", "ui-kit/flutter/outgoing-call", "ui-kit/flutter/call-buttons", diff --git a/ui-kit/flutter/events.mdx b/ui-kit/flutter/events.mdx index aafde12a6..ba3b04fe9 100644 --- a/ui-kit/flutter/events.mdx +++ b/ui-kit/flutter/events.mdx @@ -113,6 +113,18 @@ class _YourScreenState extends State with CometChatGroupEventListene `CometChatMessageEvents` emits events related to messages. +Pin, save and thread-subscription actions taken on a Kit surface are announced here, each carrying the full updated message so a custom listing can add or drop a row without a refetch: + +1. `ccMessagePinned` / `ccMessageUnpinned`: Triggered when the logged-in user pins or unpins a message. +2. `ccMessageSaved` / `ccMessageUnsaved`: Triggered when the logged-in user saves or unsaves a message. +3. `ccThreadSubscriptionChanged`: Triggered when the logged-in user follows or unfollows a thread. Carries `(int parentMessageId, bool subscribed)` rather than a message. An unfollow also hard-deletes the thread's row server-side, so remove it locally. + + + +All five are default no-ops on `CometChatMessageEventListener`, so existing listeners need no change to keep compiling. + + + ```dart diff --git a/ui-kit/flutter/message-header.mdx b/ui-kit/flutter/message-header.mdx index f723cfbb7..3c8ed43a7 100644 --- a/ui-kit/flutter/message-header.mdx +++ b/ui-kit/flutter/message-header.mdx @@ -145,6 +145,35 @@ The component listens to these SDK events internally. No manual setup needed. | `appBarOptions` | `List?` | `null` | Additional widgets in the app bar (e.g., call buttons, menu) | | `hideUserStatus` | `bool?` | `false` | Hide online/offline status for users | | `disableTypingIndicator` | `bool?` | `false` | Disable typing indicator display | +| `pinnedMessagesVisibility` | `bool?` | `true` | Show the pinned-messages entry in the ⋯ overflow menu | +| `onPinnedMessagesTap` | `VoidCallback?` | `null` | Handle the pinned-messages entry yourself instead of letting the Kit push the screen | +| `onPinnedMessageItemTap` | `Function(BaseMessage)?` | `null` | Forwarded to the pinned list — fires when a pinned row is tapped | +| `pinnedMessagesStyle` | `CometChatPinnedMessagesStyle?` | `null` | Styling for the pinned list the header opens | +| `parentMessage` | `BaseMessage?` | `null` | Puts the header in thread mode and renders the subscription bell | +| `threadSubscriptionVisibility` | `bool?` | `true` | Show the thread subscription bell (requires `parentMessage`) | + +--- + +### Pinned messages + +When `pinnedMessagesVisibility` is not `false`, the ⋯ overflow menu carries an entry that opens the conversation's [Pinned Messages](/ui-kit/flutter/pinned-messages). The entry additionally requires the server Pin feature flag (`CometChat.isPinMessageEnabled()`), and never renders in thread mode — that is, when `parentMessage` is set. + +By default the Kit pushes the screen itself. Set `onPinnedMessagesTap` to take over presentation — a desktop layout that shows the list in a side panel rather than a pushed route, say. When the Kit does present it, `onPinnedMessageItemTap` forwards row taps so you can jump the message list. + + + +```dart +CometChatMessageHeader( + group: group, + onPinnedMessageItemTap: (message) => _controller.jumpToMessage(message.id), +) +``` + + + +### Thread subscription + +Passing the thread's root message as `parentMessage` puts the header in thread mode and renders a notification bell reflecting the live subscription state. The bell is gated on `UIKitSettings.enableThreadSubscription`, which is `false` by default — see the [Threaded Messages guide](/ui-kit/flutter/guide-threaded-messages). --- diff --git a/ui-kit/flutter/message-list.mdx b/ui-kit/flutter/message-list.mdx index d3f6b090a..783494637 100644 --- a/ui-kit/flutter/message-list.mdx +++ b/ui-kit/flutter/message-list.mdx @@ -456,6 +456,7 @@ To stage and send multiple attachments, see [Message Composer](/ui-kit/flutter/m | `hideUnpinMessageOption` | `false` | Hide "Unpin" (shown while the message is pinned) | | `hideSaveMessageOption` | `false` | Hide "Save" (shown while the message is unsaved) | | `hideUnsaveMessageOption` | `false` | Hide "Unsave" (shown while the message is saved) | +| `hideThreadSubscriptionOption` | `false` | Hide "Subscribe/Unsubscribe to thread" (requires the feature gate, below) | | `hideMessagePrivatelyOption` | `false` | Hide "Message Privately" | | `hideReactionOption` | `false` | Hide "Reaction" | | `hideReplyInThreadOption` | `false` | Hide "Reply in Thread" | @@ -573,6 +574,36 @@ CometChatMessageList( +### Jump to a message + +`goToMessageId` is read once when the list mounts, so it cannot re-aim a list that is already on screen — the case you hit when a pinned or saved row is tapped and the conversation is already open. + +`CometChatMessageListController` covers that. Create it in your `State`, pass it as `controller`, and call `jumpToMessage`: the list scrolls to the message and highlights it, fetching the page around it first when it is not loaded yet. + + + +```dart +final _controller = CometChatMessageListController(); + +CometChatMessageList( + user: user, + controller: _controller, +) + +// later — e.g. from CometChatPinnedMessages.onItemTap +await _controller.jumpToMessage(message.id); +``` + + + +It attaches on mount and detaches on dispose. Calls made while no list is attached return `false` rather than throwing, and `isAttached` reports the current state. + + + +A different class of the same name existed in v5, where it exposed list state. That one is replaced by `MessageListBloc` — see [Upgrading from v5](/ui-kit/flutter/upgrading-from-v5). The v6 `CometChatMessageListController` does only imperative jumps. + + + ### Mark a message as unread Expose the "Mark as Unread" option in the long-press menu: diff --git a/ui-kit/flutter/pinned-messages.mdx b/ui-kit/flutter/pinned-messages.mdx new file mode 100644 index 000000000..3d69453bf --- /dev/null +++ b/ui-kit/flutter/pinned-messages.mdx @@ -0,0 +1,96 @@ +--- +title: "Pinned Messages" +description: "A screen listing every pinned message in one conversation, with an unpin affordance and tap-to-jump." +--- + +`CometChatPinnedMessages` lists the pinned messages of a single conversation. Rows are read-only previews ordered newest-pinned-first; each carries an Unpin affordance (permission-gated) and reports taps through `onItemTap` so the host can jump its message list to the tapped message. + +--- + +## Where It Fits + +The screen opens on the **nearest** `Navigator`, the same navigation threads use. On a desktop side-by-side layout that replaces only the chat column; on mobile it is a full-screen route. + +The usual entry point is the pinned-messages item in `CometChatMessageHeader`'s ⋯ overflow menu, which pushes this screen for you. See [Message Header](/ui-kit/flutter/message-header) for the props that control it. + +--- + +## Quick Start + +Either call the `show` helper, which pushes the screen with the kit's own transition, or embed the widget directly. + + + +```dart +CometChatPinnedMessages.show( + context, + group: group, // or: user: user + onItemTap: (message) => _controller.jumpToMessage(message.id), +); +``` + + + +Exactly one of `user` or `group` must be passed — the constructor asserts on it. + +--- + +## Actions and Events + +### Callback Methods + +| Property | Description | +| --- | --- | +| `onItemTap` | Fires when a row is tapped, **after** the screen pops. Use it to jump the message list to that message. | + +Pair it with [`CometChatMessageListController.jumpToMessage`](/ui-kit/flutter/message-list) to land on the message without remounting the list. + +### Real-Time Updates (Automatic) + +The list stays current without a refetch. No setup needed. + +| Source | Internal behavior | +| --- | --- | +| `ccMessagePinned` / `ccMessageUnpinned` (UI Kit events) | Adds or drops the row for actions taken elsewhere in the app | +| `onMessagePinned` / `onMessageUnpinned` (SDK listener) | Reflects pins and unpins made by other members | + +--- + +## Functionality + +| Property | Type | Default | Description | +| --- | --- | --- | --- | +| `user` | `User?` | - | 1:1 conversation scope. Mutually exclusive with `group`. | +| `group` | `Group?` | - | Group conversation scope. Mutually exclusive with `user`. | +| `onItemTap` | `Function(BaseMessage)?` | - | Fires on row tap, after the screen pops | +| `hideUnpinOption` | `bool?` | `false` | Hides the per-row unpin affordance even when the user has permission | +| `showBackButton` | `bool` | `true` | Toggles the header's close affordance. Turn it off when a host-owned panel supplies its own. | +| `hideAppBar` | `bool` | `false` | Drops the header entirely. Set it when the host already renders a title bar — a desktop side panel does, and two stacked headers is the result otherwise. | +| `style` | `CometChatPinnedMessagesStyle?` | - | Styling overrides | + +--- + +## Style + +`CometChatPinnedMessagesStyle` accepts `backgroundColor`, `appBarColor`, `titleTextStyle`, `itemTitleTextStyle`, `itemSubtitleTextStyle`, `itemDateTextStyle`, `iconColor`, `unpinIconColor`, `separatorColor` and `borderRadius`. + + + +```dart +CometChatPinnedMessages( + group: group, + style: CometChatPinnedMessagesStyle( + unpinIconColor: Colors.redAccent, + ), +) +``` + + + +--- + +## Next Steps + +- [Saved Messages](/ui-kit/flutter/saved-messages) +- [Message List](/ui-kit/flutter/message-list) +- [Pin Messages (SDK)](/sdk/flutter/pin-messages) diff --git a/ui-kit/flutter/saved-messages.mdx b/ui-kit/flutter/saved-messages.mdx new file mode 100644 index 000000000..246eb32bf --- /dev/null +++ b/ui-kit/flutter/saved-messages.mdx @@ -0,0 +1,92 @@ +--- +title: "Saved Messages" +description: "A screen listing the logged-in user's saved messages across every conversation." +--- + +`CometChatSavedMessages` lists the logged-in user's saved (bookmarked) messages across **every** conversation — unlike [Pinned Messages](/ui-kit/flutter/pinned-messages), it is not scoped to one chat. Rows are read-only previews ordered newest-saved-first, each with a conversation-context line and an Unsave affordance. + +Saves are private to the logged-in user and sync across their own devices. + +--- + +## Quick Start + + + +```dart +Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => CometChatSavedMessages( + onItemTap: (message) { + // open the message's conversation, then jump to it + }, + ), + ), +); +``` + + + +Because a saved message can belong to any conversation, `onItemTap` usually has to open the conversation first and then jump — see [`jumpToMessage`](/ui-kit/flutter/message-list). + +--- + +## Actions and Events + +### Callback Methods + +| Property | Description | +| --- | --- | +| `onItemTap` | Fires when a row is tapped. Use it to open the message's conversation and jump to it. | + +### Real-Time Updates (Automatic) + +| Source | Internal behavior | +| --- | --- | +| `ccMessageSaved` / `ccMessageUnsaved` (UI Kit events) | Adds or drops the row for actions taken elsewhere in the app | +| `onMessageSaved` / `onMessageUnsaved` (SDK listener) | Reflects saves made on the user's other devices | + +--- + +## Functionality + +| Property | Type | Default | Description | +| --- | --- | --- | --- | +| `onItemTap` | `Function(BaseMessage)?` | - | Fires on row tap | +| `hideUnsaveOption` | `bool?` | `false` | Hides the per-row unsave affordance | +| `popOnItemTap` | `bool` | `true` | Pops this screen **before** `onItemTap` fires. Right for a pushed mobile route; set `false` when the list lives in a persistent panel that should survive the jump. | +| `showBackButton` | `bool` | `true` | Shows the app-bar back button | +| `useCloseButton` | `bool` | `false` | Swaps the leading back arrow for a trailing ✕ with the title left-aligned — the chrome a desktop panel uses, where the screen is dismissed rather than navigated back from. Only read when `showBackButton` is `true`. | +| `style` | `CometChatSavedMessagesStyle?` | - | Styling overrides | + +### Responsive presentation + +`popOnItemTap` and `useCloseButton` exist to let one component serve both layouts: a pushed mobile route that pops on jump, and a desktop panel that stays put and closes with a ✕. + + + +```dart +CometChatSavedMessages( + onItemTap: _openSavedMessage, + popOnItemTap: !isDesktop, + useCloseButton: isDesktop, +) +``` + + + +--- + +## Style + +`CometChatSavedMessagesStyle` accepts `backgroundColor`, `appBarColor`, `titleTextStyle`, `itemTitleTextStyle`, `itemSubtitleTextStyle`, `itemContextTextStyle`, `itemDateTextStyle`, `iconColor`, `unsaveIconColor` and `separatorColor`. + +`itemContextTextStyle` styles the conversation-context line, which has no counterpart in the pinned list. + +--- + +## Next Steps + +- [Pinned Messages](/ui-kit/flutter/pinned-messages) +- [Message List](/ui-kit/flutter/message-list) +- [Save Messages (SDK)](/sdk/flutter/save-messages) diff --git a/ui-kit/flutter/threaded-messages-header.mdx b/ui-kit/flutter/threaded-messages-header.mdx index 0d0c357b0..d616c63f6 100644 --- a/ui-kit/flutter/threaded-messages-header.mdx +++ b/ui-kit/flutter/threaded-messages-header.mdx @@ -90,6 +90,26 @@ Prerequisites: CometChat SDK initialized, a user logged in, and a valid `BaseMes ### Callback Methods +#### `onThreadSubscriptionChange` + +Fires after the follow/unfollow toggle **succeeds**, with the parent message id and the new subscribed state. Use it to keep your own thread list in step — an unfollow also hard-deletes the thread's row server-side, so drop it locally. + +The bell renders only when `UIKitSettings.enableThreadSubscription` is `true`; it is `false` by default. Set `threadSubscriptionVisibility: false` to hide it when the header above already carries one. + + + +```dart +CometChatThreadedHeader( + parentMessage: parentMessage, + loggedInUser: loggedInUser, + onThreadSubscriptionChange: (parentMessageId, subscribed) { + if (!subscribed) _removeThreadRow(parentMessageId); + }, +) +``` + + + #### `onBack` Fires when the user presses the back button. @@ -145,6 +165,8 @@ CometChatThreadedHeader( | `showBackButton` | `bool?` | `true` | Toggle back button visibility | | `title` | `String?` | `null` | Custom title text | | `hideMessageComposer` | `bool?` | `false` | Hide the message composer | +| `threadSubscriptionVisibility` | `bool?` | `true` | Show the follow/unfollow bell. Requires `UIKitSettings.enableThreadSubscription`. | +| `onThreadSubscriptionChange` | `Function(int, bool)?` | `null` | Called after a successful toggle with the parent message id and the new state | --- From f3e021541a9c5ec03626aa4c6d693f7959aea30f Mon Sep 17 00:00:00 2001 From: Anshuman-cc Date: Mon, 7 Sep 2026 20:24:01 +0530 Subject: [PATCH 5/8] docs(flutter): document RichTextEditingController.getMentionRanges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last undocumented piece of 6.1.1's public surface. It came back with the trailing-toolbar restore and is genuinely public: the controller is exported through the rich_text_toolbar barrel, which cometchat_chat_uikit re-exports. Documented where it is actually useful — a trailing toolbar action that restyles a range and should leave mentions with the styling the mentions formatter gives them. Notes that the ranges come from the attached CometChatMentionsFormatter, are sorted by start offset, and are empty when no mentions formatter is attached; the example guards the cast, since a plain TextEditingController has no such method. Co-Authored-By: Claude Opus 5 --- ui-kit/flutter/message-composer.mdx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/ui-kit/flutter/message-composer.mdx b/ui-kit/flutter/message-composer.mdx index 6879f2fe1..5aaf28f85 100644 --- a/ui-kit/flutter/message-composer.mdx +++ b/ui-kit/flutter/message-composer.mdx @@ -493,6 +493,29 @@ On web, tapping a toolbar button can blur the text field and collapse the select +#### Leaving mentions alone + +A toolbar action that restyles a range will happily run over a mention, which should keep the styling the mentions formatter gives it. `RichTextEditingController.getMentionRanges()` returns the mention ranges currently tracked in the text, sorted by start offset, so an action can skip them. + +The ranges come from the attached `CometChatMentionsFormatter` — the same source the field styles mentions from — so they stay in step with the text as it is edited. The list is empty when no mentions formatter is attached, which is also what you get on a plain `TextEditingController`, so guard the cast. + + + +```dart +onToolbarTap: (context, controller) { + final mentions = controller is RichTextEditingController + ? controller.getMentionRanges() + : const []; + + bool overlapsMention(int start, int end) => + mentions.any((r) => start < r.end && end > r.start); + + // ...apply your styling only where overlapsMention() is false +}, +``` + + + *** ## Multiple Attachments From 5b376e9261989d40fcead660af77fd5867feac7a Mon Sep 17 00:00:00 2001 From: Anshuman-cc Date: Mon, 7 Sep 2026 20:27:37 +0530 Subject: [PATCH 6/8] docs(flutter): correct ThreadsRequest usage and document getMessageDetails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checked the Flutter thread-subscription page against the SDK's combined 5.0.7 branch, where the thread work now sits alongside the notification changes. - fetchNext() takes required onSuccess/onError callbacks, matching every other request builder in the SDK. The snippet awaited a bare fetchNext(), which does not compile. - Document setUid()/setGuid() for scoping a thread list to one conversation, and note that setting both fails validation at fetch time. - Note that a ThreadsRequest is single-use, so a refresh means building a new request rather than re-running an exhausted one. - Add getMessageDetails(), which carries the subscription opt-in and is the supported way to re-read threadSubscribed for a single message — the flag is absent on socket-delivered messages. Every identifier on the page verified against lib/ on the SDK branch. Co-Authored-By: Claude Opus 5 --- sdk/flutter/threaded-messages.mdx | 47 +++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/sdk/flutter/threaded-messages.mdx b/sdk/flutter/threaded-messages.mdx index 060421064..b6ae0f6fb 100644 --- a/sdk/flutter/threaded-messages.mdx +++ b/sdk/flutter/threaded-messages.mdx @@ -222,6 +222,25 @@ Two rules to internalise: Because subscribing is idempotent, rendering an un-told `false` as the un-followed affordance is safe — an unnecessary subscribe is harmless. +### Re-read the State for One Message + +When you need an authoritative flag for a single message — after acting on a socket-delivered message, or to build a thread row without refetching a list — use `getMessageDetails()`. It takes a message id and carries the subscription opt-in, so the message it resolves with has `threadSubscribed` populated. + + + +```dart +int messageId = 103; + +CometChat.getMessageDetails(messageId, onSuccess: (BaseMessage message) { + debugPrint("Thread subscribed: ${message.threadSubscribed}"); +}, onError: (CometChatException e) { + debugPrint("Message details fetching failed with exception: ${e.message}"); +}); +``` + + + + ### Reacting to Replies A thread reply is an ordinary message with `parentMessageId` set, delivered through the standard `MessageListener` alongside every other message — there is no thread-specific event channel. The same listener also delivers `onMessageEdited` and `onMessageDeleted` for replies, which a replies-only channel would miss. @@ -249,7 +268,7 @@ Your own replies do not arrive on a listener — update your UI from the send ca ### 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. Presence in this list *is* a subscription — every row is subscribed. +You can fetch the threads the logged-in user participates in by using the `ThreadsRequest` class. The `ThreadsRequestBuilder` builds the request using `setLimit()` and `setParticipatedByMe()`; once you have the `ThreadsRequest` object, call `fetchNext()` to get the next set of threads. Presence in this list *is* a subscription — every row is subscribed. @@ -258,9 +277,31 @@ ThreadsRequest threadsRequest = (ThreadsRequestBuilder() ..setParticipatedByMe(true) ..setLimit(30)).build(); -List threads = await threadsRequest.fetchNext(); -debugPrint("Fetched ${threads.length} threads"); +threadsRequest.fetchNext(onSuccess: (List threads) { + debugPrint("Fetched ${threads.length} threads"); +}, onError: (CometChatException e) { + debugPrint("Thread fetching failed with exception: ${e.message}"); +}); ``` + +To scope the list to a single conversation, add `setUid()` for a 1-1 counterpart or `setGuid()` for a group. The two are mutually exclusive — setting both is a validation error raised when you call `fetchNext()`. + + + +```dart +ThreadsRequest threadsRequest = (ThreadsRequestBuilder() + ..setGuid("cometchat-guid-1") + ..setLimit(30)).build(); +``` + + + + + + +A `ThreadsRequest` is single-use and one-directional: it accumulates its paging state internally and has no reset. To refresh a list, build a new request from the builder and replace the list rather than re-running an exhausted one. This is the same contract as `ConversationsRequest` and `MessagesRequest`. + + From d6d3bd5d7a3c1bd17b374b1f09daa44443169286 Mon Sep 17 00:00:00 2001 From: Anshuman-cc Date: Mon, 7 Sep 2026 20:34:18 +0530 Subject: [PATCH 7/8] docs(notifications): Flutter quoted replies use QuotedRepliesOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Flutter tabs typed the quoted-replies preference as RepliesOptions. The Flutter SDK declares it as its own type: QuotedRepliesOptions? quotedReplies; on both GroupPreferences and OneOnOnePreferences, so the snippets did not compile. The type is deliberately separate from RepliesOptions — values 1-3 coincide, but an option is only meaningful within its own preference, and the distinct type makes a cross-assignment a compile error rather than a silently wrong setting. Fixes the two read snippets and the two constructor snippets. Only the Flutter tabs change; the JavaScript, Android and iOS tabs keep RepliesOptions, which is correct for those SDKs. Co-Authored-By: Claude Opus 5 --- notifications/preferences.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/notifications/preferences.mdx b/notifications/preferences.mdx index cecbda53b..be3d6aa99 100644 --- a/notifications/preferences.mdx +++ b/notifications/preferences.mdx @@ -164,7 +164,7 @@ CometChatNotifications.fetchPreferences( MessagesOptions? messagesPreference = groupPreferences?.messages; RepliesOptions? repliesPreference = groupPreferences?.replies; - RepliesOptions? quotedRepliesPreference = groupPreferences?.quotedReplies; + QuotedRepliesOptions? quotedRepliesPreference = groupPreferences?.quotedReplies; ReactionsOptions? reactionsPreference = groupPreferences?.reactions; MemberActionsOptions? memberAddedPreference = groupPreferences?.memberAdded; MemberActionsOptions? memberJoinedPreference = groupPreferences?.memberJoined; @@ -323,7 +323,7 @@ NotificationPreferences updatedPreferences = NotificationPreferences(); GroupPreferences groupPreferences = GroupPreferences( messages: MessagesOptions.SUBSCRIBE_TO_MENTIONS, replies: RepliesOptions.SUBSCRIBE_TO_ALL, - quotedReplies: RepliesOptions.SUBSCRIBE_TO_ALL, + quotedReplies: QuotedRepliesOptions.SUBSCRIBE_TO_ALL, reactions: ReactionsOptions.SUBSCRIBE_TO_REACTIONS_ON_ALL_MESSAGES, memberAdded: MemberActionsOptions.SUBSCRIBE, memberJoined: MemberActionsOptions.SUBSCRIBE, @@ -444,7 +444,7 @@ CometChatNotifications.fetchPreferences( MessagesOptions? oneOnOneMessagesPreference = oneOnOnePreferences?.messages; RepliesOptions? oneOnOneRepliesPreference = oneOnOnePreferences?.replies; - RepliesOptions? oneOnOneQuotedRepliesPreference = oneOnOnePreferences?.quotedReplies; + QuotedRepliesOptions? oneOnOneQuotedRepliesPreference = oneOnOnePreferences?.quotedReplies; ReactionsOptions? oneOnOneReactionsPreference = oneOnOnePreferences?.reactions; }, onError: (e) { @@ -567,7 +567,7 @@ NotificationPreferences updatedPreferences = NotificationPreferences(); OneOnOnePreferences oneOnOnePreferences = OneOnOnePreferences( messages: MessagesOptions.SUBSCRIBE_TO_ALL, replies: RepliesOptions.SUBSCRIBE_TO_MENTIONS, - quotedReplies: RepliesOptions.SUBSCRIBE_TO_MENTIONS, + quotedReplies: QuotedRepliesOptions.SUBSCRIBE_TO_MENTIONS, reactions: ReactionsOptions.SUBSCRIBE_TO_REACTIONS_ON_ALL_MESSAGES); // Load the updates in the NotificationPreferences instance. From 57dff27a090e45c95906e7a5a6cb5a5e8065f706 Mon Sep 17 00:00:00 2001 From: Anshuman-cc Date: Mon, 7 Sep 2026 20:35:53 +0530 Subject: [PATCH 8/8] docs(notifications): note the FCM_WEB provider for Flutter web builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Flutter SDK gained PushPlatforms.FCM_WEB ('fcm_web') for registering an FCM token from a web build, but the push guides cover only Flutter on Android and iOS. Adds a note under the platform cards pointing web builds at the right provider and the fcmToken argument, and says the dashboard setup is unchanged from the Flutter (Android) guide. A full Flutter web walkthrough is left for when there is a sample app to base it on — the existing Flutter guides are walkthroughs of the UI Kit sample, which has no web variant. Co-Authored-By: Claude Opus 5 --- notifications/push-overview.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/notifications/push-overview.mdx b/notifications/push-overview.mdx index 6d83a545f..2c2ff934b 100644 --- a/notifications/push-overview.mdx +++ b/notifications/push-overview.mdx @@ -76,3 +76,9 @@ UI Kit implementation + + + +There is no separate guide for Flutter web builds yet. A Flutter app running on the web registers its FCM token with `PushPlatforms.FCM_WEB` (wire value `fcm_web`) instead of `FCM_FLUTTER_ANDROID` or the APNs providers, passing the token as the `fcmToken` argument to `CometChatNotifications.registerPushToken()`. Everything else — enabling push and adding an FCM provider in the dashboard — matches the Flutter (Android) guide. + +