From 3bc6449a4dcf2a9bcf1f548b40d7bc8903a7f0bf Mon Sep 17 00:00:00 2001 From: Hritika Date: Fri, 7 Aug 2026 17:26:52 +0530 Subject: [PATCH 1/2] docs(android): thread subscription, pin & save messages, pin conversations, and composer trailing buttons (SDK v5 + UI Kit) --- docs.json | 8 + .../v5/additional-message-filtering.mdx | 100 +++++ sdk/android/v5/pin-conversation.mdx | 283 ++++++++++++++ sdk/android/v5/pin-message.mdx | 353 ++++++++++++++++++ sdk/android/v5/real-time-listeners.mdx | 100 +++++ sdk/android/v5/save-message.mdx | 298 +++++++++++++++ sdk/android/v5/thread-subscription.mdx | 330 ++++++++++++++++ sdk/android/v5/threaded-messages.mdx | 4 + ui-kit/android/components-overview.mdx | 2 + ui-kit/android/conversations.mdx | 13 + ui-kit/android/core-features.mdx | 32 ++ .../android/customization-text-formatters.mdx | 7 + ui-kit/android/customization-view-slots.mdx | 3 + ui-kit/android/events.mdx | 31 ++ .../android/guide-pin-and-save-messages.mdx | 166 ++++++++ ui-kit/android/guide-thread-subscription.mdx | 157 ++++++++ ui-kit/android/guide-threaded-messages.mdx | 3 + ui-kit/android/message-composer.mdx | 71 ++++ ui-kit/android/message-header.mdx | 34 ++ ui-kit/android/message-list.mdx | 11 + ui-kit/android/methods.mdx | 20 + ui-kit/android/pinned-messages.mdx | 149 ++++++++ ui-kit/android/saved-messages.mdx | 139 +++++++ ui-kit/android/threaded-messages-header.mdx | 42 ++- 24 files changed, 2355 insertions(+), 1 deletion(-) create mode 100644 sdk/android/v5/pin-conversation.mdx create mode 100644 sdk/android/v5/pin-message.mdx create mode 100644 sdk/android/v5/save-message.mdx create mode 100644 sdk/android/v5/thread-subscription.mdx create mode 100644 ui-kit/android/guide-pin-and-save-messages.mdx create mode 100644 ui-kit/android/guide-thread-subscription.mdx create mode 100644 ui-kit/android/pinned-messages.mdx create mode 100644 ui-kit/android/saved-messages.mdx diff --git a/docs.json b/docs.json index cb43da08b..7f33f7c67 100644 --- a/docs.json +++ b/docs.json @@ -1839,6 +1839,8 @@ "ui-kit/android/call-buttons", "ui-kit/android/call-logs", "ui-kit/android/search", + "ui-kit/android/pinned-messages", + "ui-kit/android/saved-messages", "ui-kit/android/ai-assistant-chat-history", "ui-kit/android/notification-feed" ] @@ -1855,6 +1857,8 @@ "pages": [ "ui-kit/android/guide-overview", "ui-kit/android/guide-threaded-messages", + "ui-kit/android/guide-thread-subscription", + "ui-kit/android/guide-pin-and-save-messages", "ui-kit/android/guide-block-unblock-user", "ui-kit/android/guide-new-chat", "ui-kit/android/guide-message-privately", @@ -4176,10 +4180,14 @@ "sdk/android/v5/additional-message-filtering", "sdk/android/v5/retrieve-conversations", "sdk/android/v5/threaded-messages", + "sdk/android/v5/thread-subscription", "sdk/android/v5/edit-message", "sdk/android/v5/delete-message", "sdk/android/v5/flag-message", + "sdk/android/v5/pin-message", + "sdk/android/v5/save-message", "sdk/android/v5/delete-conversation", + "sdk/android/v5/pin-conversation", "sdk/android/v5/typing-indicators", "sdk/android/v5/delivery-read-receipts", "sdk/android/v5/transient-messages", diff --git a/sdk/android/v5/additional-message-filtering.mdx b/sdk/android/v5/additional-message-filtering.mdx index 6532fb806..f903220ab 100644 --- a/sdk/android/v5/additional-message-filtering.mdx +++ b/sdk/android/v5/additional-message-filtering.mdx @@ -1270,3 +1270,103 @@ val UID = "cometchat-uid-1" + +## Pinned messages + +*In other words, how do I fetch the pinned messages of a conversation* + +This can be achieved by setting the pinned flag to true using the `setPinned()` method. Setting a `UID` or a `GUID` is mandatory for this filter — pinned messages are always scoped to a single conversation. The returned list is sorted by the time of pinning, most recently pinned first. + + + +```java +String UID = "cometchat-uid-1"; + +MessagesRequest messagesRequest = new MessagesRequest.MessagesRequestBuilder() + .setPinned(true) + .setLimit(50) + .setUID(UID) + .build(); +``` + + + + +```java +String GUID = "cometchat-guid-1"; + +MessagesRequest messagesRequest = new MessagesRequest.MessagesRequestBuilder() + .setPinned(true) + .setLimit(50) + .setGUID(GUID) + .build(); +``` + + + + +```kotlin +val UID = "cometchat-uid-1" + +val messagesRequest = MessagesRequestBuilder() + .setPinned(true) + .setLimit(50) + .setUID(UID) + .build() +``` + + + + +```kotlin +val GUID = "cometchat-guid-1" + +val messagesRequest = MessagesRequestBuilder() + .setPinned(true) + .setLimit(50) + .setGUID(GUID) + .build() +``` + + + + + +For the full pin workflow — pinning, unpinning, limits and real-time events — see [Pin A Message](/sdk/android/v5/pin-message). + +## Saved messages + +*In other words, how do I fetch the messages the logged-in user has saved* + +This can be achieved by setting the saved flag to true using the `setSaved()` method. Saved messages are private to the logged-in user and span all of their conversations, so this filter must **not** be combined with `setUID()` or `setGUID()`. The returned list is sorted by the time of saving, most recently saved first. + + + +```java +MessagesRequest messagesRequest = new MessagesRequest.MessagesRequestBuilder() + .setSaved(true) + .setLimit(50) + .build(); +``` + + + + +```kotlin +val messagesRequest = MessagesRequestBuilder() + .setSaved(true) + .setLimit(50) + .build() +``` + + + + + + + +The pinned and saved lists paginate forward-only on an internal server cursor. `fetchNext()` works the same from the caller's side, but message-window filters such as `setMessageId()` and `setTimestamp()` do not apply to these two queries. + + + +For the full save workflow — saving, unsaving, limits and real-time events — see [Save A Message](/sdk/android/v5/save-message). diff --git a/sdk/android/v5/pin-conversation.mdx b/sdk/android/v5/pin-conversation.mdx new file mode 100644 index 000000000..415d10b5d --- /dev/null +++ b/sdk/android/v5/pin-conversation.mdx @@ -0,0 +1,283 @@ +--- +title: "Pin A Conversation" +--- + +Let users keep their most important chats at the top of the list. Pinning a conversation is **per-user** — it changes the order of the acting user's own conversation list and is synced across their devices. A conversation can also be pinned globally for everyone by the app itself (a system pin). Let's see how to work with pinned conversations in CometChat's Android SDK. + + + +Pinning a conversation with the SDK requires the Pin Conversation feature to be enabled for your app. You can check its availability at runtime using the [feature flag](#feature-availability). + + + +## Pin a Conversation + +To pin a conversation, use the `pinConversation` method. Pass the `UID` of the other user (for a one-on-one conversation) or the `GUID` of the group, along with the matching conversation type. On success, the callback returns the updated `Conversation` with its pin attributes set. + + + +```java +String UID = "cometchat-uid-1"; + +CometChat.pinConversation(UID, CometChatConstants.CONVERSATION_TYPE_USER, + new CometChat.CallbackListener() { + @Override + public void onSuccess(Conversation conversation) { + Log.d(TAG, "Conversation pinned at: " + conversation.getPinnedAt()); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed to pin conversation: " + e.getMessage()); + } +}); +``` + + + + +```java +String GUID = "cometchat-guid-1"; + +CometChat.pinConversation(GUID, CometChatConstants.CONVERSATION_TYPE_GROUP, + new CometChat.CallbackListener() { + @Override + public void onSuccess(Conversation conversation) { + Log.d(TAG, "Conversation pinned at: " + conversation.getPinnedAt()); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed to pin conversation: " + e.getMessage()); + } +}); +``` + + + + +```kotlin +val UID = "cometchat-uid-1" + +CometChat.pinConversation(UID, CometChatConstants.CONVERSATION_TYPE_USER, + object : CometChat.CallbackListener() { + override fun onSuccess(conversation: Conversation?) { + Log.d(TAG, "Conversation pinned at: ${conversation?.pinnedAt}") + } + + override fun onError(e: CometChatException?) { + Log.e(TAG, "Failed to pin conversation: ${e?.message}") + } +}) +``` + + + + +```kotlin +val GUID = "cometchat-guid-1" + +CometChat.pinConversation(GUID, CometChatConstants.CONVERSATION_TYPE_GROUP, + object : CometChat.CallbackListener() { + override fun onSuccess(conversation: Conversation?) { + Log.d(TAG, "Conversation pinned at: ${conversation?.pinnedAt}") + } + + override fun onError(e: CometChatException?) { + Log.e(TAG, "Failed to pin conversation: ${e?.message}") + } +}) +``` + + + + + +## Unpin a Conversation + +To unpin a conversation, use the `unpinConversation` method with the same parameters. On success, the callback returns the updated `Conversation` with its pin attributes cleared. + + + +```java +String UID = "cometchat-uid-1"; + +CometChat.unpinConversation(UID, CometChatConstants.CONVERSATION_TYPE_USER, + new CometChat.CallbackListener() { + @Override + public void onSuccess(Conversation conversation) { + Log.d(TAG, "Conversation unpinned. isPinned: " + conversation.isPinned()); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed to unpin conversation: " + e.getMessage()); + } +}); +``` + + + + +```kotlin +val UID = "cometchat-uid-1" + +CometChat.unpinConversation(UID, CometChatConstants.CONVERSATION_TYPE_USER, + object : CometChat.CallbackListener() { + override fun onSuccess(conversation: Conversation?) { + Log.d(TAG, "Conversation unpinned. isPinned: ${conversation?.isPinned}") + } + + override fun onError(e: CometChatException?) { + Log.e(TAG, "Failed to unpin conversation: ${e?.message}") + } +}) +``` + + + + + + + +A user cannot unpin a **system pin** (a conversation pinned globally by the app). Use `isSystemPinned()` to detect this case and hide the unpin action. + + + +## Fetch Pinned Conversations + +The default conversations list already orders pinned conversations at the top. To fetch **only** pinned conversations, use the `setPinnedBy()` filter of the `ConversationsRequestBuilder`. + +| Value | Description | +| --------------- | -------------------------------------------------------- | +| `"me"` | Conversations pinned by the logged-in user. | +| `"system"` | Conversations pinned globally by the app (system pins). | +| `"system,me"` | Both. | + + + +```java +ConversationsRequest conversationsRequest = new ConversationsRequest.ConversationsRequestBuilder() + .setPinnedBy("system,me") + .setLimit(30) + .build(); + +conversationsRequest.fetchNext(new CometChat.CallbackListener>() { + @Override + public void onSuccess(List conversations) { + Log.d(TAG, "Pinned conversations: " + conversations.size()); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Fetch failed: " + e.getMessage()); + } +}); +``` + + + + +```kotlin +val conversationsRequest = ConversationsRequest.ConversationsRequestBuilder() + .setPinnedBy("system,me") + .setLimit(30) + .build() + +conversationsRequest.fetchNext(object : CometChat.CallbackListener>() { + override fun onSuccess(conversations: List?) { + Log.d(TAG, "Pinned conversations: ${conversations?.size}") + } + + override fun onError(e: CometChatException?) { + Log.e(TAG, "Fetch failed: ${e?.message}") + } +}) +``` + + + + + +## Check if a Conversation is Pinned + +Every fetched `Conversation` carries its pin state. + +| Method | Description | +| ------------------ | ------------------------------------------------------------------------------------------------ | +| `isPinned()` | Returns `true` if the conversation is pinned for the logged-in user (or globally). | +| `isSystemPinned()` | Returns `true` if the conversation was pinned globally by the app (`pinnedBy` is `app_system`). | +| `getPinnedAt()` | The timestamp at which the conversation was pinned. `0` when it is not pinned. | +| `getPinnedBy()` | The `UID` of the pinner, or `app_system` for a system pin. | + +## Real-time Conversation Pin Events + +Register a `ConversationListener` to be notified when a conversation is pinned or unpinned, so your list can reorder without a refetch. + +Today these callbacks fire on the **acting user's device** when a pin or unpin succeeds. Delivery to the user's other devices activates once server-side real-time delivery for conversation-pin events is rolled out — until then, other sessions pick up the change on their next conversations fetch. + + + +```java +private String listenerID = "UNIQUE_LISTENER_ID"; + +CometChat.addConversationListener(listenerID, new CometChat.ConversationListener() { + @Override + public void onConversationPinned(Conversation conversation) { + Log.d(TAG, "Conversation pinned: " + conversation.getConversationId()); + } + + @Override + public void onConversationUnpinned(Conversation conversation) { + Log.d(TAG, "Conversation unpinned: " + conversation.getConversationId()); + } +}); +``` + + + + +```kotlin +val listenerID = "UNIQUE_LISTENER_ID" + +CometChat.addConversationListener(listenerID, object : CometChat.ConversationListener() { + override fun onConversationPinned(conversation: Conversation) { + Log.d(TAG, "Conversation pinned: ${conversation.conversationId}") + } + + override fun onConversationUnpinned(conversation: Conversation) { + Log.d(TAG, "Conversation unpinned: ${conversation.conversationId}") + } +}) +``` + + + + + +To stop listening, remove the listener with `CometChat.removeConversationListener(listenerID)`. + +## Feature Availability + +Check whether the Pin Conversation feature is enabled for your app before showing pin actions in your UI. The method is synchronous and safe to call from the UI layer. + + + +```java +if (CometChat.isPinConversationEnabled()) { + // show the Pin conversation option +} +``` + + + + +```kotlin +if (CometChat.isPinConversationEnabled()) { + // show the Pin conversation option +} +``` + + + + diff --git a/sdk/android/v5/pin-message.mdx b/sdk/android/v5/pin-message.mdx new file mode 100644 index 000000000..56b79dace --- /dev/null +++ b/sdk/android/v5/pin-message.mdx @@ -0,0 +1,353 @@ +--- +title: "Pin A Message" +--- + +Keep important messages easy to find by pinning them to a conversation. A pinned message is visible to **all participants** of the conversation, along with who pinned it and when. Users can pin messages, unpin them, and fetch all pinned messages of a conversation. You can also listen to pin events in real-time. Let's see how to work with pinned messages in CometChat's Android SDK. + + + +Pinning a message with the SDK requires the Pin Message feature to be enabled for your app. You can check its availability at runtime using the [feature flag](#feature-availability). + + + +## Pin a Message + +To pin a message, use the `pinMessage` method and pass the ID of the message to be pinned. On success, the callback returns the updated `BaseMessage` with its pin attributes set. + + + +```java +long messageId = 1; + +CometChat.pinMessage(messageId, new CometChat.CallbackListener() { + @Override + public void onSuccess(BaseMessage message) { + Log.d(TAG, "Message pinned at: " + message.getPinnedAt()); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed to pin message: " + e.getMessage()); + } +}); +``` + + + + +```kotlin +val messageId = 1L + +CometChat.pinMessage(messageId, object : CometChat.CallbackListener() { + override fun onSuccess(message: BaseMessage?) { + Log.d(TAG, "Message pinned at: ${message?.pinnedAt}") + } + + override fun onError(e: CometChatException?) { + Log.e(TAG, "Failed to pin message: ${e?.message}") + } +}) +``` + + + + + + + +In a **group conversation**, the CometChat UI Kits show the Pin/Unpin option only to participants with the **Admin** or **Moderator** scope, or the group **owner**. This gate is applied client-side — the SDK does not currently enforce roles on the server, so apply your own role check if you build custom pin UI. Every participant can see pinned messages. In a **one-on-one conversation**, both participants can pin and unpin. Deleted messages cannot be pinned; deleting a pinned message automatically unpins it. + + + +## Unpin a Message + +To unpin a message, use the `unpinMessage` method. Any participant with pin permission can unpin a message — not just the user who originally pinned it. On success, the callback returns the updated `BaseMessage` with its pin attributes cleared. + + + +```java +long messageId = 1; + +CometChat.unpinMessage(messageId, new CometChat.CallbackListener() { + @Override + public void onSuccess(BaseMessage message) { + Log.d(TAG, "Message unpinned. isPinned: " + message.isPinned()); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed to unpin message: " + e.getMessage()); + } +}); +``` + + + + +```kotlin +val messageId = 1L + +CometChat.unpinMessage(messageId, object : CometChat.CallbackListener() { + override fun onSuccess(message: BaseMessage?) { + Log.d(TAG, "Message unpinned. isPinned: ${message?.isPinned}") + } + + override fun onError(e: CometChatException?) { + Log.e(TAG, "Failed to unpin message: ${e?.message}") + } +}) +``` + + + + + +## Fetch Pinned Messages + +To fetch all pinned messages of a conversation, create a `MessagesRequest` with the `setPinned(true)` filter of the `MessagesRequestBuilder`. Setting a `UID` (for a one-on-one conversation) or a `GUID` (for a group) is **mandatory** — exactly one of the two. The returned list is sorted by the time of pinning, most recently pinned first. + + + +```java +String UID = "cometchat-uid-1"; + +MessagesRequest messagesRequest = new MessagesRequest.MessagesRequestBuilder() + .setPinned(true) + .setLimit(50) + .setUID(UID) + .build(); + +messagesRequest.fetchNext(new CometChat.CallbackListener>() { + @Override + public void onSuccess(List messages) { + Log.d(TAG, "Pinned messages: " + messages.size()); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Pinned messages fetch failed: " + e.getMessage()); + } +}); +``` + + + + +```java +String GUID = "cometchat-guid-1"; + +MessagesRequest messagesRequest = new MessagesRequest.MessagesRequestBuilder() + .setPinned(true) + .setLimit(50) + .setGUID(GUID) + .build(); + +messagesRequest.fetchNext(new CometChat.CallbackListener>() { + @Override + public void onSuccess(List messages) { + Log.d(TAG, "Pinned messages: " + messages.size()); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Pinned messages fetch failed: " + e.getMessage()); + } +}); +``` + + + + +```kotlin +val UID = "cometchat-uid-1" + +val messagesRequest = MessagesRequest.MessagesRequestBuilder() + .setPinned(true) + .setLimit(50) + .setUID(UID) + .build() + +messagesRequest.fetchNext(object : CometChat.CallbackListener>() { + override fun onSuccess(messages: List?) { + Log.d(TAG, "Pinned messages: ${messages?.size}") + } + + override fun onError(e: CometChatException?) { + Log.e(TAG, "Pinned messages fetch failed: ${e?.message}") + } +}) +``` + + + + +```kotlin +val GUID = "cometchat-guid-1" + +val messagesRequest = MessagesRequest.MessagesRequestBuilder() + .setPinned(true) + .setLimit(50) + .setGUID(GUID) + .build() + +messagesRequest.fetchNext(object : CometChat.CallbackListener>() { + override fun onSuccess(messages: List?) { + Log.d(TAG, "Pinned messages: ${messages?.size}") + } + + override fun onError(e: CometChatException?) { + Log.e(TAG, "Pinned messages fetch failed: ${e?.message}") + } +}) +``` + + + + + + + +The pinned-messages list paginates forward-only on an internal server cursor. `fetchNext()` works the same as any other `MessagesRequest` from the caller's side, but message-window filters such as `setMessageId()` and `setTimestamp()` do not apply to this query. See [Additional Message Filtering](/sdk/android/v5/additional-message-filtering) for all the filters of the `MessagesRequestBuilder` class. + + + +## Check if a Message is Pinned + +Every fetched or received message carries its pin state on the `BaseMessage` itself. + +| Method | Description | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `isPinned()` | Returns `true` if the message is currently pinned in its conversation. | +| `getPinnedAt()` | The timestamp at which the message was pinned. `0` when the message is not pinned. | +| `getPinnedBy()` | The `UID` of the user who most recently pinned the message. The value `app_system` indicates a pin applied by the app itself (a system pin). | + + + +```java +if (message.isPinned()) { + Log.d(TAG, "Pinned by " + message.getPinnedBy() + " at " + message.getPinnedAt()); +} +``` + + + + +```kotlin +if (message.isPinned) { + Log.d(TAG, "Pinned by ${message.pinnedBy} at ${message.pinnedAt}") +} +``` + + + + + + + +Editing a message preserves its pin. A message stores only its most recent pinner in `getPinnedBy()`. + + + +## Real-time Pin Events + +Register a `MessageListener` and override the pin callbacks. Each event delivers the full updated `BaseMessage`, so you can directly replace the message in your list. + +Today these callbacks fire on the **acting user's device** when a pin or unpin succeeds. Delivery to other participants activates once server-side real-time delivery for pin events is rolled out — until then, other clients pick up pin changes on their next message fetch. + + + +```java +private String listenerID = "UNIQUE_LISTENER_ID"; + +CometChat.addMessageListener(listenerID, new CometChat.MessageListener() { + @Override + public void onMessagePinned(BaseMessage message) { + Log.d(TAG, "Message pinned: " + message.getId()); + } + + @Override + public void onMessageUnpinned(BaseMessage message) { + Log.d(TAG, "Message unpinned: " + message.getId()); + } +}); +``` + + + + +```kotlin +val listenerID = "UNIQUE_LISTENER_ID" + +CometChat.addMessageListener(listenerID, object : CometChat.MessageListener() { + override fun onMessagePinned(message: BaseMessage) { + Log.d(TAG, "Message pinned: ${message.id}") + } + + override fun onMessageUnpinned(message: BaseMessage) { + Log.d(TAG, "Message unpinned: ${message.id}") + } +}) +``` + + + + + +To stop listening, remove the listener with `CometChat.removeMessageListener(listenerID)`. + +## Pin Limit + +A conversation can hold a limited number of pinned messages (100 by default). When the limit is exceeded, the SDK surfaces the server error through `onError`, and the applicable limit can be read programmatically from the exception — never hard-code it. + + + +```java +@Override +public void onError(CometChatException e) { + Object limit = e.getErrorParams() != null ? e.getErrorParams().get("limit") : null; + if (limit != null) { + Log.e(TAG, "You can pin up to " + limit + " messages in a conversation."); + } +} +``` + + + + +```kotlin +override fun onError(e: CometChatException?) { + val limit = e?.errorParams?.get("limit") + if (limit != null) { + Log.e(TAG, "You can pin up to $limit messages in a conversation.") + } +} +``` + + + + + +## Feature Availability + +Check whether the Pin Message feature is enabled for your app before showing pin actions in your UI. The method is synchronous and safe to call from the UI layer. + + + +```java +if (CometChat.isPinMessageEnabled()) { + // show the Pin option +} +``` + + + + +```kotlin +if (CometChat.isPinMessageEnabled()) { + // show the Pin option +} +``` + + + + diff --git a/sdk/android/v5/real-time-listeners.mdx b/sdk/android/v5/real-time-listeners.mdx index 78a3620ee..e0d0eab6d 100644 --- a/sdk/android/v5/real-time-listeners.mdx +++ b/sdk/android/v5/real-time-listeners.mdx @@ -175,6 +175,10 @@ The `MessageListener` class provides you with live events related to messages. B | `onMessagesRead(MessageReceipt messageReceipt)` | This event is triggered when a set of messages are marked as read for any particular conversation. | | `onMessageEdited(BaseMessage message)` | This method is triggered when a particular message has been edited in a user/group conversation. | | `onMessageDeleted(BaseMessage message)` | This event is triggered when a particular message is deleted in a user/group conversation. | +| `onMessagePinned(BaseMessage message)` | This event is triggered when a message is pinned in a user/group conversation. | +| `onMessageUnpinned(BaseMessage message)` | This event is triggered when a message is unpinned in a user/group conversation. | +| `onMessageSaved(BaseMessage message)` | This event is triggered when the logged-in user saves a message (private — never delivered to other participants). | +| `onMessageUnsaved(BaseMessage message)` | This event is triggered when the logged-in user unsaves a message (private — never delivered to other participants). | | `onInteractiveMessageReceived(InteractiveMessage message)` | This event is triggered when an Interactive Message is received. | | `onInteractionGoalCompleted(InteractionReceipt receipt)` | This event is triggered when an interaction Goal is achieved. | | `onTransientMessageReceived(TransientMessage transientMessage)` | This event is triggered when a Transient Message is received. | @@ -354,6 +358,102 @@ where `UNIQUE_LISTENER_ID` is the unique identifier for the listener. Please mak Once the activity/fragment where the `MessageListener` is declared is not in use, you need to remove the listener using the `removeMessageListener()` method which takes the id of the listener to be removed as the parameter. We suggest you call this method in the `onPause()` method of the activity/fragment. +## Conversation Listener + +The `ConversationListener` class provides you with live events related to conversations. Below are the callback methods provided by the `ConversationListener` class. + +| Method | Information | +| ------------------------------------------------- | ---------------------------------------------------------------------------- | +| `onConversationPinned(Conversation conversation)` | This event is triggered when a conversation is pinned for the logged-in user. | +| `onConversationUnpinned(Conversation conversation)` | This event is triggered when a conversation is unpinned for the logged-in user. | + +To add the `ConversationListener`, you need to use the `addConversationListener()` method provided by the `CometChat` class. + + + +```java +CometChat.addConversationListener(UNIQUE_LISTENER_ID, new CometChat.ConversationListener() { + @Override + public void onConversationPinned(Conversation conversation) { + + } + + @Override + public void onConversationUnpinned(Conversation conversation) { + + } +}); +``` + + + + +```kotlin +CometChat.addConversationListener(UNIQUE_LISTENER_ID, object : CometChat.ConversationListener() { + override fun onConversationPinned(conversation: Conversation) { + + } + + override fun onConversationUnpinned(conversation: Conversation) { + + } +}) +``` + + + + + +Once you have successfully registered the listener and no longer wish to receive any events, you need to remove the listener using the `removeConversationListener()` method with the same `UNIQUE_LISTENER_ID`. + +## Thread Listener + +The `ThreadListener` class provides you with live events related to thread subscriptions and threaded replies. Below are the callback methods provided by the `ThreadListener` class. + +| Method | Information | +| ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| `onThreadSubscriptionChanged(ThreadSubscriptionEvent event)` | This event is triggered when the logged-in user's subscription state for a thread changes. | +| `onThreadReplyReceived(ThreadReplyEvent event)` | This event is triggered when a reply is received in a thread. | + +To add the `ThreadListener`, you need to use the `addThreadListener()` method provided by the `CometChat` class. + + + +```java +CometChat.addThreadListener(UNIQUE_LISTENER_ID, new CometChat.ThreadListener() { + @Override + public void onThreadSubscriptionChanged(ThreadSubscriptionEvent event) { + + } + + @Override + public void onThreadReplyReceived(ThreadReplyEvent event) { + + } +}); +``` + + + + +```kotlin +CometChat.addThreadListener(UNIQUE_LISTENER_ID, object : CometChat.ThreadListener() { + override fun onThreadSubscriptionChanged(event: ThreadSubscriptionEvent) { + + } + + override fun onThreadReplyReceived(event: ThreadReplyEvent) { + + } +}) +``` + + + + + +Once you have successfully registered the listener and no longer wish to receive any events, you need to remove the listener using the `removeThreadListener()` method with the same `UNIQUE_LISTENER_ID`. See [Thread Subscription](/sdk/android/v5/thread-subscription) for the full feature. + ## AI Assistant Listener The `AIAssistantListener` class provides you with real-time streaming events from AI Agent runs. These events are emitted during a run lifecycle and include tool calls, card generation, and text message streaming. For a complete overview of the event lifecycle, see [AI Agents](/sdk/android/ai-agents). diff --git a/sdk/android/v5/save-message.mdx b/sdk/android/v5/save-message.mdx new file mode 100644 index 000000000..e57cabe99 --- /dev/null +++ b/sdk/android/v5/save-message.mdx @@ -0,0 +1,298 @@ +--- +title: "Save A Message" +--- + +Let users bookmark messages for later. Saving a message is **private to the logged-in user** — nobody else in the conversation can see it — and works **across conversations**: a user's saved messages from all of their chats appear in one list, synced across all of their devices. Let's see how to work with saved messages in CometChat's Android SDK. + + + +Saving a message with the SDK requires the Save Message feature to be enabled for your app. You can check its availability at runtime using the [feature flag](#feature-availability). + + + +## Save a Message + +To save a message, use the `saveMessage` method and pass the ID of the message. On success, the callback returns the updated `BaseMessage` with its `savedAt` attribute set. + + + +```java +long messageId = 1; + +CometChat.saveMessage(messageId, new CometChat.CallbackListener() { + @Override + public void onSuccess(BaseMessage message) { + Log.d(TAG, "Message saved at: " + message.getSavedAt()); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed to save message: " + e.getMessage()); + } +}); +``` + + + + +```kotlin +val messageId = 1L + +CometChat.saveMessage(messageId, object : CometChat.CallbackListener() { + override fun onSuccess(message: BaseMessage?) { + Log.d(TAG, "Message saved at: ${message?.savedAt}") + } + + override fun onError(e: CometChatException?) { + Log.e(TAG, "Failed to save message: ${e?.message}") + } +}) +``` + + + + + + + +Unlike pinning, saving has no role restrictions — every user can save any message they have access to. Deleted messages cannot be saved. + + + +## Unsave a Message + +To remove a message from the user's saved list, use the `unsaveMessage` method. On success, the callback returns the updated `BaseMessage` with its `savedAt` attribute cleared. + + + +```java +long messageId = 1; + +CometChat.unsaveMessage(messageId, new CometChat.CallbackListener() { + @Override + public void onSuccess(BaseMessage message) { + Log.d(TAG, "Message unsaved. isSaved: " + message.isSaved()); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed to unsave message: " + e.getMessage()); + } +}); +``` + + + + +```kotlin +val messageId = 1L + +CometChat.unsaveMessage(messageId, object : CometChat.CallbackListener() { + override fun onSuccess(message: BaseMessage?) { + Log.d(TAG, "Message unsaved. isSaved: ${message?.isSaved}") + } + + override fun onError(e: CometChatException?) { + Log.e(TAG, "Failed to unsave message: ${e?.message}") + } +}) +``` + + + + + +## Fetch Saved Messages + +To fetch all messages the logged-in user has saved, create a `MessagesRequest` with the `setSaved(true)` filter of the `MessagesRequestBuilder`. Because saved messages are user-level and span conversations, you must **not** set a `UID` or `GUID`. The returned list is sorted by the time of saving, most recently saved first. + + + +```java +MessagesRequest messagesRequest = new MessagesRequest.MessagesRequestBuilder() + .setSaved(true) + .setLimit(50) + .build(); + +messagesRequest.fetchNext(new CometChat.CallbackListener>() { + @Override + public void onSuccess(List messages) { + Log.d(TAG, "Saved messages: " + messages.size()); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Saved messages fetch failed: " + e.getMessage()); + } +}); +``` + + + + +```kotlin +val messagesRequest = MessagesRequest.MessagesRequestBuilder() + .setSaved(true) + .setLimit(50) + .build() + +messagesRequest.fetchNext(object : CometChat.CallbackListener>() { + override fun onSuccess(messages: List?) { + Log.d(TAG, "Saved messages: ${messages?.size}") + } + + override fun onError(e: CometChatException?) { + Log.e(TAG, "Saved messages fetch failed: ${e?.message}") + } +}) +``` + + + + + + + +The saved-messages list paginates forward-only on an internal server cursor. `fetchNext()` works the same as any other `MessagesRequest` from the caller's side, but message-window filters such as `setMessageId()` and `setTimestamp()` do not apply to this query. Use each returned message's `getReceiverType()` and receiver to resolve which conversation it belongs to. See [Additional Message Filtering](/sdk/android/v5/additional-message-filtering) for all the filters of the `MessagesRequestBuilder` class. + + + + + +If the user loses access to a conversation (for example, they are removed from a group), messages saved from it are cleaned up and no longer returned. + + + +## Check if a Message is Saved + +Every fetched message carries the logged-in user's save state on the `BaseMessage` itself. These values are **per-user**: the same message shows different values to different users. + +| Method | Description | +| -------------- | --------------------------------------------------------------------------------------- | +| `isSaved()` | Returns `true` if the logged-in user has saved this message. | +| `getSavedAt()` | The timestamp at which the logged-in user saved the message. `0` when it is not saved. | + + + +```java +if (message.isSaved()) { + Log.d(TAG, "Saved at " + message.getSavedAt()); +} +``` + + + + +```kotlin +if (message.isSaved) { + Log.d(TAG, "Saved at ${message.savedAt}") +} +``` + + + + + +## Real-time Save Events + +Because saving is private, save events are never delivered to other participants. Register a `MessageListener` and override the save callbacks; each event delivers the full updated `BaseMessage`. + +Today these callbacks fire on the **acting device** when a save or unsave succeeds. Delivery to the user's other devices activates once server-side real-time delivery for save events is rolled out — until then, other sessions pick up save changes on their next fetch. + + + +```java +private String listenerID = "UNIQUE_LISTENER_ID"; + +CometChat.addMessageListener(listenerID, new CometChat.MessageListener() { + @Override + public void onMessageSaved(BaseMessage message) { + Log.d(TAG, "Message saved: " + message.getId()); + } + + @Override + public void onMessageUnsaved(BaseMessage message) { + Log.d(TAG, "Message unsaved: " + message.getId()); + } +}); +``` + + + + +```kotlin +val listenerID = "UNIQUE_LISTENER_ID" + +CometChat.addMessageListener(listenerID, object : CometChat.MessageListener() { + override fun onMessageSaved(message: BaseMessage) { + Log.d(TAG, "Message saved: ${message.id}") + } + + override fun onMessageUnsaved(message: BaseMessage) { + Log.d(TAG, "Message unsaved: ${message.id}") + } +}) +``` + + + + + +To stop listening, remove the listener with `CometChat.removeMessageListener(listenerID)`. + +## Save Limit + +A user can save a limited number of messages (100 by default). When the limit is exceeded, the SDK surfaces the server error through `onError`, and the applicable limit can be read programmatically from `CometChatException.getErrorParams()` under the `limit` key — never hard-code it. + + + +```java +@Override +public void onError(CometChatException e) { + Object limit = e.getErrorParams() != null ? e.getErrorParams().get("limit") : null; + if (limit != null) { + Log.e(TAG, "You can save up to " + limit + " messages."); + } +} +``` + + + + +```kotlin +override fun onError(e: CometChatException?) { + val limit = e?.errorParams?.get("limit") + if (limit != null) { + Log.e(TAG, "You can save up to $limit messages.") + } +} +``` + + + + + +## Feature Availability + +Check whether the Save Message feature is enabled for your app before showing save actions in your UI. The method is synchronous and safe to call from the UI layer. + + + +```java +if (CometChat.isSaveMessageEnabled()) { + // show the Save option +} +``` + + + + +```kotlin +if (CometChat.isSaveMessageEnabled()) { + // show the Save option +} +``` + + + + diff --git a/sdk/android/v5/thread-subscription.mdx b/sdk/android/v5/thread-subscription.mdx new file mode 100644 index 000000000..75ea2bf38 --- /dev/null +++ b/sdk/android/v5/thread-subscription.mdx @@ -0,0 +1,330 @@ +--- +title: "Thread Subscription" +--- + +Give users Slack-style control over thread noise. A user can **subscribe** to a message thread to be notified of future replies, or **unsubscribe** from it to mute it. Users are automatically subscribed to a thread when they start it, reply in it, or are @-mentioned in it — and they can explicitly subscribe to any parent message, even one that has no replies yet. The SDK also exposes the list of threads a user participates in, so you can build a thread inbox. Let's see how to work with thread subscriptions in CometChat's Android SDK. + + + +Thread subscription builds on [Threaded Messages](/sdk/android/v5/threaded-messages). A thread is identified by the ID of its **parent message** — there is no separate thread ID. + + + +## Subscribe to a Thread + +To subscribe to a thread, use the `subscribeToThread` method with the ID of the thread's parent message. The call is **idempotent** — subscribing to a thread the user is already subscribed to succeeds silently. Subscribing to a message with zero replies is allowed; the user will be notified when the first reply arrives. + + + +```java +long parentMessageId = 1; + +CometChat.subscribeToThread(parentMessageId, new CometChat.CallbackListener() { + @Override + public void onSuccess(String response) { + Log.d(TAG, "Subscribed to thread: " + response); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed to subscribe: " + e.getMessage()); + } +}); +``` + + + + +```kotlin +val parentMessageId = 1L + +CometChat.subscribeToThread(parentMessageId, object : CometChat.CallbackListener() { + override fun onSuccess(response: String?) { + Log.d(TAG, "Subscribed to thread: $response") + } + + override fun onError(e: CometChatException?) { + Log.e(TAG, "Failed to subscribe: ${e?.message}") + } +}) +``` + + + + + +## Unsubscribe from a Thread + +To unsubscribe from a thread, use the `unsubscribeFromThread` method. This too is idempotent — unsubscribing from a thread the user is not subscribed to succeeds silently. + + + +```java +long parentMessageId = 1; + +CometChat.unsubscribeFromThread(parentMessageId, new CometChat.CallbackListener() { + @Override + public void onSuccess(String response) { + Log.d(TAG, "Unsubscribed from thread: " + response); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed to unsubscribe: " + e.getMessage()); + } +}); +``` + + + + +```kotlin +val parentMessageId = 1L + +CometChat.unsubscribeFromThread(parentMessageId, object : CometChat.CallbackListener() { + override fun onSuccess(response: String?) { + Log.d(TAG, "Unsubscribed from thread: $response") + } + + override fun onError(e: CometChatException?) { + Log.e(TAG, "Failed to unsubscribe: ${e?.message}") + } +}) +``` + + + + + + + +Unsubscribing is **not sticky**. If the user replies in the thread again, or is @-mentioned in it, they are automatically re-subscribed. Do not promise users "you won't be notified about this thread again". + + + +## Get the Subscription State + +`getThreadSubscriptionState` returns the logged-in user's subscription state for a thread **synchronously** — it never makes a network call, never throws, and is safe to call from your UI while rendering. + + + +```java +ThreadSubscriptionState state = CometChat.getThreadSubscriptionState(parentMessageId); + +switch (state) { + case SUBSCRIBED: /* render "Unsubscribe from thread" */ break; + case NOT_SUBSCRIBED: /* render "Subscribe to thread" */ break; + case UNKNOWN: /* render "Subscribe to thread" */ break; +} +``` + + + + +```kotlin +when (CometChat.getThreadSubscriptionState(parentMessageId)) { + ThreadSubscriptionState.SUBSCRIBED -> { /* render "Unsubscribe from thread" */ } + ThreadSubscriptionState.NOT_SUBSCRIBED -> { /* render "Subscribe to thread" */ } + ThreadSubscriptionState.UNKNOWN -> { /* render "Subscribe to thread" */ } +} +``` + + + + + +The state is a deliberate tri-state, not a boolean: + +| Value | Meaning | +| ---------------- | ------------------------------------------------------------------------------------------------- | +| `SUBSCRIBED` | The user is subscribed to this thread and will be notified of replies. | +| `NOT_SUBSCRIBED` | The user is known not to be subscribed to this thread. | +| `UNKNOWN` | The state has not been learned yet (for example, the message arrived live over the websocket). | + + + +Render `UNKNOWN` as the unsubscribed state (an enabled "Subscribe" control) — never as a spinner or a disabled control. The state is kept in an in-memory, per-login-session cache; it is cleared on login and logout, and nothing is persisted to disk. + + + + + +The cache is seeded **only** by message fetches that opt in with `withThreadSubscribed(true)` on the `MessagesRequestBuilder` — a plain fetch does not carry the subscription state, and `getThreadSubscriptionState` will keep returning `UNKNOWN`. Opt in on the requests that back your thread UI: + +```kotlin +val messagesRequest = MessagesRequest.MessagesRequestBuilder() + .setUID(UID) + .setLimit(50) + .withThreadSubscribed(true) + .build() +``` + +(The CometChat UI Kit sets this flag internally, so this only concerns you when calling the SDK directly.) + + + +## Fetch the Threads a User Participates In + +To build a thread inbox — one row per thread the user is part of — create a `ThreadsRequest` using the `ThreadsRequestBuilder`. The list is the union of threads the user started, replied in, was mentioned in, or explicitly subscribed to. Every returned row is, by definition, a thread the user is subscribed to: **participation is subscription**, and unsubscribing removes the row. + +| Setting | Description | +| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `setLimit(int value)` | Page size, validated between 1 and 1000. Defaults to 30 — thread rows are heavy (each carries a root message and a last reply). | +| `setUid(String value)` | Scope the list to threads in the one-on-one conversation with this user. Mutually exclusive with `setGuid()`. | +| `setGuid(String value)` | Scope the list to threads in this group. Mutually exclusive with `setUid()`. | +| `setParticipatedByMe(boolean)` | Defaults to `true`. Only the threads the logged-in user participates in are returned. | + + + +```java +ThreadsRequest threadsRequest = new ThreadsRequest.ThreadsRequestBuilder() + .setLimit(30) + .build(); + +threadsRequest.fetchNext(new CometChat.CallbackListener>() { + @Override + public void onSuccess(List threads) { + for (MessageThread thread : threads) { + Log.d(TAG, "Thread " + thread.getParentMessageId() + + " has " + thread.getReplyCount() + " replies"); + } + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Threads fetch failed: " + e.getMessage()); + } +}); +``` + + + + +```kotlin +val threadsRequest = ThreadsRequest.ThreadsRequestBuilder() + .setLimit(30) + .build() + +threadsRequest.fetchNext(object : CometChat.CallbackListener>() { + override fun onSuccess(threads: List?) { + threads?.forEach { thread -> + Log.d(TAG, "Thread ${thread.parentMessageId} has ${thread.replyCount} replies") + } + } + + override fun onError(e: CometChatException?) { + Log.e(TAG, "Threads fetch failed: ${e?.message}") + } +}) +``` + + + + + +Call `fetchNext()` repeatedly to page forward; `hasMore()` tells you whether more pages exist. A `ThreadsRequest` is **single-use and forward-only** — there is no `fetchPrevious()`. To refresh the list from the top, build a new request from the builder and replace your list with its results. Calling `fetchNext()` while a fetch is already in flight fails with a request-in-progress error. + +### The MessageThread Model + +Each row is a `MessageThread`: + +| Method | Description | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `getParentMessageId()` | The thread's identity — the ID of its root message. | +| `getParentMessage()` | The root message as a full `BaseMessage`. | +| `getReplyCount()` | Number of replies in the thread. | +| `getLastReply()` | The most recent reply as a `BaseMessage`. `null` for a thread with no replies yet — expected, not an error. | +| `getConversationId()` | The ID of the conversation the thread belongs to. | +| `getReceiverType()` | `user` or `group`. | +| `getReceiverUid()` | The raw `UID`/`GUID` of the conversation. Resolve the display name and avatar yourself via `CometChat.getUser()` / `CometChat.getGroup()`. | +| `getSubscriptionState()` | Always `SUBSCRIBED` for rows in this list. | +| `getUnreadReplyCount()` | Reserved for future use — currently `null` (unknown), which is not the same as `0`. | +| `getUpdatedAt()` | An internal pagination cursor. **Do not sort your UI on it.** | + + + +To order rows in your UI, sort on `getLastReply().getSentAt()`, falling back to `getParentMessage().getSentAt()` for zero-reply threads — not on `getUpdatedAt()`. + + + + + +The list starts **empty** for every user when the feature launches — it fills up as users reply, get mentioned, and subscribe to threads. There is no historical backfill. + + + +## Real-time Thread Events + +Register a `ThreadListener` to keep your UI in sync as subscription state changes and replies arrive. + + + +```java +private String listenerID = "UNIQUE_LISTENER_ID"; + +CometChat.addThreadListener(listenerID, new CometChat.ThreadListener() { + @Override + public void onThreadSubscriptionChanged(ThreadSubscriptionEvent event) { + Log.d(TAG, "Thread " + event.getParentMessageId() + + " is now " + event.getSubscriptionState()); + } + + @Override + public void onThreadReplyReceived(ThreadReplyEvent event) { + Log.d(TAG, "New reply in thread " + event.getParentMessageId() + + ": " + event.getReply().getId()); + } +}); +``` + + + + +```kotlin +val listenerID = "UNIQUE_LISTENER_ID" + +CometChat.addThreadListener(listenerID, object : CometChat.ThreadListener() { + override fun onThreadSubscriptionChanged(event: ThreadSubscriptionEvent) { + Log.d(TAG, "Thread ${event.parentMessageId} is now ${event.subscriptionState}") + } + + override fun onThreadReplyReceived(event: ThreadReplyEvent) { + Log.d(TAG, "New reply in thread ${event.parentMessageId}: ${event.reply.id}") + } +}) +``` + + + + + +To stop listening, remove the listener with `CometChat.removeThreadListener(listenerID)`. + +- `onThreadSubscriptionChanged` fires when the logged-in user's subscription state for a thread changes on **this device** — after a successful subscribe/unsubscribe call, or after a threaded send auto-subscribes them. +- `onThreadReplyReceived` fires for every incoming threaded message and for the user's own successful threaded sends. Use it to bump reply counts and re-sort your thread list. + + + +Registering a second listener with the same `listenerID` **replaces** the first one. Use distinct IDs for distinct screens. A subscribe or unsubscribe performed on the user's **other device** does not currently produce a real-time event on this one — the state self-corrects on the next message fetch, so refresh your thread list when the app returns to the foreground. + + + +## Notification Preferences + +The notification preference for replies gains a new value so users can be notified only for threads they are subscribed to: `SUBSCRIBE_TO_SUBSCRIBED_THREADS` in the `RepliesOptions` enum. + +| Value | Behavior | +| -------------------------------- | ---------------------------------------------------------------- | +| `DONT_SUBSCRIBE` | No notifications for thread replies. | +| `SUBSCRIBE_TO_ALL` | Notifications for all thread replies. | +| `SUBSCRIBE_TO_MENTIONS` | Notifications only for replies that mention the user. | +| `SUBSCRIBE_TO_SUBSCRIBED_THREADS`| Notifications for replies in threads the user is subscribed to. | + +See [Notification Preferences](/notifications) for how to read and update a user's preferences. + +## Error Handling + +| Error | Meaning | +| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ERR_MESSAGE_NO_ACCESS` | The user no longer has access to the message's conversation (for example, they left or were banned from the group). Treat the thread as inaccessible and remove its row. | +| `ERR_MESSAGE_ID_NOT_FOUND` | The parent message does not exist (for example, it was deleted). | diff --git a/sdk/android/v5/threaded-messages.mdx b/sdk/android/v5/threaded-messages.mdx index 114e6f114..c86cc743f 100644 --- a/sdk/android/v5/threaded-messages.mdx +++ b/sdk/android/v5/threaded-messages.mdx @@ -241,3 +241,7 @@ messagesRequest.fetchPrevious(object : CallbackListener?>() { 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. + +## Subscribe to a Thread + +Users can subscribe to or unsubscribe from a thread to control whether they are notified about its replies, and you can fetch the list of threads a user participates in to build a thread inbox. See [Thread Subscription](/sdk/android/v5/thread-subscription). diff --git a/ui-kit/android/components-overview.mdx b/ui-kit/android/components-overview.mdx index 5b99dac80..70b3b71bb 100644 --- a/ui-kit/android/components-overview.mdx +++ b/ui-kit/android/components-overview.mdx @@ -48,6 +48,8 @@ Components communicate via `CometChatEvents` — a SharedFlow-based event bus. S | `CometChatMessageList` | Message feed with reactions, receipts, threads | [Message List](/ui-kit/android/message-list) | | `CometChatMessageComposer` | Rich input with attachments, mentions, voice | [Message Composer](/ui-kit/android/message-composer) | | `CometChatThreadHeader` | Parent message bubble and reply count | [Thread Header](/ui-kit/android/threaded-messages-header) | +| `CometChatPinnedMessages` | Full-screen list of a conversation's pinned messages | [Pinned Messages](/ui-kit/android/pinned-messages) | +| `CometChatSavedMessages` | Full-screen, private list of the user's saved messages | [Saved Messages](/ui-kit/android/saved-messages) | ### Calling diff --git a/ui-kit/android/conversations.mdx b/ui-kit/android/conversations.mdx index 269d48ad8..1e71ad293 100644 --- a/ui-kit/android/conversations.mdx +++ b/ui-kit/android/conversations.mdx @@ -431,6 +431,7 @@ The component listens to these SDK events internally. No manual setup needed. | `setSelectionMode(MULTIPLE)` | `selectionMode = MULTIPLE` | Enable selection mode | | `setTitle("Chats")` | `title = "Chats"` | Custom toolbar title | | `setSearchPlaceholderText("Search...")` | `searchPlaceholderText = "Search..."` | Search placeholder | +| `setPinConversationOptionVisibility(View.GONE)` | — | Hide the built-in Pin/Unpin conversation option | --- @@ -708,6 +709,18 @@ CometChatConversations( +### Built-in Pin Conversation Option + +When the Pin Conversation feature is enabled for your app (`CometChatUIKit.isPinConversationEnabled()`), the long-press menu automatically includes **Pin conversation** / **Unpin conversation** — no wiring needed. Pinning applies immediately with a toast; unpinning asks for confirmation first. Pinned conversations display a pin indicator next to the timestamp and stay at the **top of the list**, holding their position even as new messages arrive in other chats. Hide the option with `setPinConversationOptionVisibility(View.GONE)`. + +To render a pinned-only list, pass a request builder with the pinned filter — see [Pin A Conversation (SDK)](/sdk/android/v5/pin-conversation): + +```kotlin lines +conversations.setConversationsRequestBuilder( + ConversationsRequest.ConversationsRequestBuilder().setPinnedBy("system,me") +) +``` + --- ## Common Patterns diff --git a/ui-kit/android/core-features.mdx b/ui-kit/android/core-features.mdx index fe18b0a5d..3bf9d7ec0 100644 --- a/ui-kit/android/core-features.mdx +++ b/ui-kit/android/core-features.mdx @@ -136,6 +136,27 @@ Address specific users in a conversation by typing `@` to trigger mention sugges | [CometChatMessageList](/ui-kit/android/message-list) | Renders mentions with distinct styling in the message flow. | +## Pin & Save Messages + +Keep important messages in reach. Pinning highlights a message for **everyone** in the conversation; saving bookmarks it **privately** for the acting user, across all of their conversations. Both come with action-sheet options, bubble indicators, and dedicated full-screen views. + +| Component | Role | +| --- | --- | +| [CometChatMessageList](/ui-kit/android/message-list) | Provides the Pin/Unpin and Save/Unsave options and shows the bubble footer indicators. | +| [CometChatPinnedMessages](/ui-kit/android/pinned-messages) | Full-screen list of a conversation's pinned messages. | +| [CometChatSavedMessages](/ui-kit/android/saved-messages) | Full-screen, private list of the user's saved messages. | +| [CometChatMessageHeader](/ui-kit/android/message-header) | Built-in "Pinned messages" menu entry point. | + +See the [Pin & Save Messages guide](/ui-kit/android/guide-pin-and-save-messages) for end-to-end wiring. + +## Pin Conversations + +Keep the chats that matter at the top. Users pin a conversation from the long-press menu; pinned conversations show a pin indicator and stay above the rest of the list. + +| Component | Role | +| --- | --- | +| [CometChatConversations](/ui-kit/android/conversations) | Provides the Pin/Unpin conversation option, the row indicator, and pinned-first ordering. | + ## Rich Text Formatting Rich Text Formatting allows users to style their messages with bold, italic, strikethrough, code, code blocks, blockquotes, ordered/unordered lists, and links. This brings richer expression to conversations and helps users emphasize key points. @@ -162,6 +183,17 @@ Respond directly to a specific message, keeping conversations organized. | [CometChatMessageComposer](/ui-kit/android/message-composer) | Allows composing messages within a thread. | | [CometChatMessageList](/ui-kit/android/message-list) | Displays threaded messages in context. | +## Thread Subscription + +Let users subscribe to or unsubscribe from a thread to control whether its replies notify them. Opt-in feature — enable it with `UIKitSettings.setEnableThreadSubscription(true)`. + +| Component | Role | +| --- | --- | +| [CometChatMessageList](/ui-kit/android/message-list) | Provides the Subscribe to thread / Unsubscribe from thread option in the message action sheet. | +| [CometChatThreadHeader](/ui-kit/android/threaded-messages-header) | Shows the subscription bell on the thread view. | + +See the [Thread Subscription guide](/ui-kit/android/guide-thread-subscription) for setup and behavior. + ## Quoted Replies Reply to specific messages by selecting "Reply" from the message action menu, maintaining context in the conversation. diff --git a/ui-kit/android/customization-text-formatters.mdx b/ui-kit/android/customization-text-formatters.mdx index ad96f7922..738976545 100644 --- a/ui-kit/android/customization-text-formatters.mdx +++ b/ui-kit/android/customization-text-formatters.mdx @@ -26,8 +26,15 @@ The abstract class takes a `trackingCharacter` that triggers the formatter when | `prepareComposerSpan(context, message, spannable)` | Apply spans to text in the message composer. | | `prepareConversationSpan(context, message, spannable)` | Apply spans to the last message preview in the conversation list. | | `handlePreMessageSend(context, message)` | Modify a message before it's sent (attach metadata, transform text). | +| `getOriginalText(text)` | Strip this formatter's display markup back to the storable token form sent on the wire. Default is identity; override it when your formatter renders a token (e.g. a custom style tag) differently from how it is stored, so the token survives send and re-renders via the `prepare*Span` overrides. | | `onItemClick(context, suggestionItem, user, group)` | Called when the user selects a suggestion item. | + + +Custom formatters render **live in the composer while typing**, and their `prepare*Span` overrides are applied consistently across the message bubble, the reply/edit preview, and the conversation-list preview — register the formatter once and every surface picks it up. + + + ### Suggestion System | Method | Description | diff --git a/ui-kit/android/customization-view-slots.mdx b/ui-kit/android/customization-view-slots.mdx index 0c367e33c..0ae6b78ff 100644 --- a/ui-kit/android/customization-view-slots.mdx +++ b/ui-kit/android/customization-view-slots.mdx @@ -254,6 +254,9 @@ View slots are available on all list-based components: | `CometChatCallLogs` | `CallLogsViewHolderListener` | `(CallLog) -> Unit` | | `CometChatReactionList` | `ReactionListViewHolderListener` | `(Reaction) -> Unit` | | `CometChatMessageHeader` | `MessageHeaderViewHolderListener` | `(User?, Group?) -> Unit` | +| `CometChatMessageComposer` (rich-text toolbar trailing slot) | `RichTextToolbarTrailingViewListener` | `RowScope.(ComposerInputController) -> Unit` | + +The composer's trailing-toolbar slot additionally hands your view a live `ComposerInputController` for reading and mutating the input — see [Message Composer › Rich-Text Toolbar Trailing Buttons](/ui-kit/android/message-composer#rich-text-toolbar-trailing-buttons). --- diff --git a/ui-kit/android/events.mdx b/ui-kit/android/events.mdx index 6575197b6..c61d5b517 100644 --- a/ui-kit/android/events.mdx +++ b/ui-kit/android/events.mdx @@ -49,6 +49,7 @@ import com.cometchat.uikit.core.events.CometChatEvents | `CometChatEvents.groupEvents` | `GroupEvent` | Group created, deleted, member changes | | `CometChatEvents.userEvents` | `UserEvent` | User blocked, unblocked | | `CometChatEvents.uiEvents` | `UIEvent` | Panel visibility, active chat changes | +| `CometChatEvents.threadEvents` | `CometChatThreadEvent` | Thread subscription state changes | ## API reference @@ -70,6 +71,10 @@ import com.cometchat.uikit.core.events.CometChatEvents | `MessageEvent.CustomInteractiveReceived(message)` | Triggered when a custom interactive message is received. | | `MessageEvent.InteractionGoalCompleted(message)` | Triggered when an interaction goal is completed. | | `MessageEvent.SchedulerReceived(message)` | Triggered when a scheduler message is received. | +| `MessageEvent.MessagePinned(message)` | Triggered when a message is pinned. | +| `MessageEvent.MessageUnpinned(message)` | Triggered when a message is unpinned. | +| `MessageEvent.MessageSaved(message)` | Triggered when the logged-in user saves a message. | +| `MessageEvent.MessageUnsaved(message)` | Triggered when the logged-in user unsaves a message. | **Collecting events:** @@ -157,6 +162,32 @@ fun MessageEventsHandler() { --- +### Thread Events + +`CometChatEvents.threadEvents` emits `CometChatThreadEvent` instances when the logged-in user subscribes to or unsubscribes from a message thread, so every surface showing a subscription control can stay in sync without a refetch. + +**Event types:** + +| Event | Description | +| ----- | ----------- | +| `CometChatThreadEvent.SubscriptionChanged(parentMessageId, subscriptionState, source)` | Triggered when the user's subscription state for a thread changes. `subscriptionState` is a `ThreadSubscriptionState` (`SUBSCRIBED` / `NOT_SUBSCRIBED` / `UNKNOWN`). | + +**Collecting events:** + +```kotlin +lifecycleScope.launch { + CometChatEvents.threadEvents.collect { event -> + when (event) { + is CometChatThreadEvent.SubscriptionChanged -> { + // Update your subscription control for event.parentMessageId + } + } + } +} +``` + +See the [Thread Subscription guide](/ui-kit/android/guide-thread-subscription) for the feature end to end. + ### Call Events `CometChatEvents.callEvents` emits `CallEvent` sealed class instances for call lifecycle changes. diff --git a/ui-kit/android/guide-pin-and-save-messages.mdx b/ui-kit/android/guide-pin-and-save-messages.mdx new file mode 100644 index 000000000..f29da0b88 --- /dev/null +++ b/ui-kit/android/guide-pin-and-save-messages.mdx @@ -0,0 +1,166 @@ +--- +title: "Pin & Save Messages" +sidebarTitle: "Pin & Save Messages" +description: "Add pinned messages, saved messages, and pinned conversations to your app with the built-in options, indicators, and screens." +--- + +## Overview + +Three related features help users keep track of what matters: + +| Feature | Scope | Visible to | Surfaces | +| --- | --- | --- | --- | +| **Pin Message** | One conversation | Everyone in it | Action-sheet option, bubble indicator, [Pinned Messages](/ui-kit/android/pinned-messages) screen | +| **Save Message** | All conversations | Only the acting user | Action-sheet option, bubble indicator, [Saved Messages](/ui-kit/android/saved-messages) screen | +| **Pin Conversation** | Conversation list | Only the acting user | Long-press option + pin indicator in [Conversations](/ui-kit/android/conversations) | + +The options, confirmation dialogs, toasts and indicators are built into the UI Kit components. The only integration work is wiring the two full-screen views into your navigation. + +## Prerequisites + +- A working message view — see [Getting Started](/ui-kit/android/getting-started). +- The features enabled for your app. Check at runtime with the flags on `CometChatUIKit`: + +```kotlin lines +CometChatUIKit.isPinMessageEnabled() +CometChatUIKit.isSaveMessageEnabled() +CometChatUIKit.isPinConversationEnabled() +``` + +## Pin & Save in the Message List + +With the features enabled, [CometChatMessageList](/ui-kit/android/message-list) automatically adds **Pin message / Unpin message** and **Save message / Unsave message** to the long-press action sheet for text and media messages. The labels toggle with the message's current state. + +- **Pin** is role-gated in groups: the UI Kit shows the Pin/Unpin option only to participants with the **Admin** or **Moderator** scope, or the group **owner**. Everyone sees pinned indicators. In one-on-one chats both participants can pin. Note this gate is applied by the UI Kit — if you build custom pin UI directly on the SDK, apply your own role check. +- **Save** has no role gating — every user can save any message. +- **Pin** and **Save** apply immediately and show a toast (*Message pinned*, *Message saved*, …); **Unpin** and **Unsave** ask for confirmation first. If a pin or save limit is exceeded, the limit toast is generated from the server's response automatically. +- Pinned and saved messages show **indicators in the bubble footer** (a filled pin / bookmark before the timestamp), updating live for all bubble types. + +## Step 1: Open Pinned Messages from the Chat Header + +[CometChatMessageHeader](/ui-kit/android/message-header) has a built-in **Pinned messages** menu item — enable it and handle the tap: + + + +```kotlin MessagesActivity.kt lines +messageHeader.setShowPinnedMessagesOption(true) + +messageHeader.setOnPinnedMessagesClickListener { + val intent = Intent(this, PinnedMessagesActivity::class.java) + user?.let { u -> intent.putExtra("uid", u.uid) } + group?.let { g -> intent.putExtra("guid", g.guid) } + pinnedMessagesLauncher.launch(intent) +} +``` + + + +Host [CometChatPinnedMessages](/ui-kit/android/pinned-messages) in that activity (or Compose destination), scoped with the same user/group as the chat. + +### Jump Back to a Pinned Message + +Return the tapped message's ID to the chat screen and scroll to it: + + + +```kotlin PinnedMessagesActivity.kt lines +pinnedMessages.setOnMessageClickListener { message -> + setResult(RESULT_OK, Intent().putExtra("goToMessageId", message.id)) // message.id is a Long + finish() +} +``` + +```kotlin MessagesActivity.kt lines +private val pinnedMessagesLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val messageId = result.data?.getLongExtra("goToMessageId", 0L) ?: 0L + if (result.resultCode == RESULT_OK && messageId != 0L) { + messageList.gotoMessage(messageId) + } + } +``` + + +```kotlin lines +CometChatPinnedMessages( + user = user, + onMessageClick = { message -> + navController.navigate(MessagesRoute(goToMessageId = message.id)) { + popUpTo { inclusive = true } + } + } +) +``` + + + +## Step 2: Open Saved Messages from Your App Chrome + +Saved messages are **user-level**, so the entry point belongs in app chrome — a profile/user menu on the conversations screen, a settings row, or a navigation tab — not inside a single chat: + + + +```kotlin ChatsFragment.kt lines +// e.g. a "Saved messages" row in the user menu of your conversations screen +savedMessagesMenuItem.isVisible = CometChatUIKit.isSaveMessageEnabled() +savedMessagesMenuItem.setOnClickListener { + startActivity(Intent(requireContext(), SavedMessagesActivity::class.java)) +} +``` + + + +Host [CometChatSavedMessages](/ui-kit/android/saved-messages) there. Because rows span conversations, opening a tapped message means resolving its source conversation first: + + + +```kotlin SavedMessagesActivity.kt lines +savedMessages.setOnMessageClickListener { message -> + val me = CometChat.getLoggedInUser()?.uid + val intent = Intent(this, MessagesActivity::class.java) + if (message.receiverType == CometChatConstants.RECEIVER_TYPE_GROUP) { + intent.putExtra("guid", (message.receiver as Group).guid) + } else { + val peer = if (message.sender.uid == me) message.receiver as User else message.sender + intent.putExtra("uid", peer.uid) + } + intent.putExtra("goToMessageId", message.id) + startActivity(intent) + finish() +} +``` + + + +## Pin Conversations + +With the feature enabled, [CometChatConversations](/ui-kit/android/conversations) adds **Pin conversation / Unpin conversation** to the long-press menu, shows a pin indicator on pinned rows, and keeps pinned conversations at the top of the list — including when new messages arrive. No wiring is required; to hide the option: + + + +```kotlin lines +conversations.setPinConversationOptionVisibility(View.GONE) +``` + + + +## Live Updates + +On the acting user's device, all surfaces stay in sync through the UI Kit event bus — pinning from the action sheet updates the bubble indicator and the Pinned Messages screen without a refetch. Delivery of pin/save events to other participants and to the user's other devices activates once server-side real-time delivery for these features is rolled out; until then, other clients pick the change up on their next fetch. If you build custom UI, observe the `MessagePinned` / `MessageUnpinned` / `MessageSaved` / `MessageUnsaved` events; see [Events](/ui-kit/android/events). + +## Summary / Feature Matrix + +| Capability | Built-in | Your wiring | +| --- | --- | --- | +| Action-sheet options, confirm dialogs, toasts | ✅ | — | +| Bubble footer indicators | ✅ | — | +| Pinned/Saved screens (list, unpin/unsave, empty states, live upkeep) | ✅ | Host + navigate | +| Chat-header "Pinned messages" entry | ✅ (opt-in) | `setShowPinnedMessagesOption(true)` + click listener | +| Saved messages entry point | — | An item in your app chrome | +| Jump-to-message | — | `gotoMessage` / navigation | +| Conversation pinning (option, indicator, ordering) | ✅ | — | + +## Next Steps & Further Reading + +- [Pinned Messages](/ui-kit/android/pinned-messages) · [Saved Messages](/ui-kit/android/saved-messages) — component references. +- [Pin A Message](/sdk/android/v5/pin-message) · [Save A Message](/sdk/android/v5/save-message) · [Pin A Conversation](/sdk/android/v5/pin-conversation) — the SDK APIs underneath. diff --git a/ui-kit/android/guide-thread-subscription.mdx b/ui-kit/android/guide-thread-subscription.mdx new file mode 100644 index 000000000..eac1658d6 --- /dev/null +++ b/ui-kit/android/guide-thread-subscription.mdx @@ -0,0 +1,157 @@ +--- +title: "Thread Subscription" +sidebarTitle: "Thread Subscription" +description: "Let users subscribe to or unsubscribe from message threads so notifications only reach the people who care." +--- + +## Overview + +Thread subscription gives users Slack-style control over thread noise: they can **subscribe** to a thread to be notified about its replies, or **unsubscribe** from one to mute it. Users are automatically subscribed when they start a thread, reply in one, or are @-mentioned in one — subscribing explicitly is how they opt in to a conversation they haven't participated in yet. + +The UI Kit ships two surfaces for the same toggle, kept in sync automatically: + +1. A **Subscribe to thread / Unsubscribe from thread** option in the message action sheet. +2. A **subscription bell** on the thread view. + +## Prerequisites + +- Threaded messages working in your app — see [Threaded Messages](/ui-kit/android/guide-threaded-messages). +- CometChat UI Kit for Android with Chat SDK v5 or later. + +## Enable the Feature + +Thread subscription is **off by default** and is enabled per app via `UIKitSettings` at init time. When the gate is off, neither surface renders and no subscription request is ever made. + + + +```kotlin lines +val uiKitSettings = UIKitSettings.UIKitSettingsBuilder() + .setAppId(APP_ID) + .setRegion(REGION) + .setAuthKey(AUTH_KEY) + .setEnableThreadSubscription(true) // opt in — default is false + .subscribePresenceForAllUsers() + .build() + +CometChatUIKit.init(this, uiKitSettings, object : CometChat.CallbackListener() { + override fun onSuccess(successString: String?) { } + override fun onError(e: CometChatException?) { } +}) +``` + + + +Anywhere you build your own UI around the feature, check the gate with: + +```kotlin lines +if (CometChatUIKit.isThreadSubscriptionEnabled()) { + // render your subscription control / entry point +} +``` + +## Surface 1: The Message Action Sheet Option + +With the gate on, [CometChatMessageList](/ui-kit/android/message-list) automatically adds a **Subscribe to thread** / **Unsubscribe from thread** option to the long-press action sheet. The label reflects the current state, and the option appears on regular messages of every type (agent messages and moderation-blocked messages are excluded) — on a thread reply it targets the thread's root message, so subscribing from anywhere in the thread works. + +To hide the option while keeping the rest of the feature: + + + +```kotlin lines +messageList.setThreadSubscriptionOptionVisibility(View.GONE) +``` + + + +## Surface 2: The Thread Header Bell + +[CometChatThreadHeader](/ui-kit/android/threaded-messages-header) renders a subscription bell as a trailing control on the reply-count bar. It flips optimistically on tap and reverts with a toast if the request fails. + + + +```kotlin lines +// Hide the bell (e.g. because you host your own — see below) +threadHeader.setThreadSubscriptionVisibility(View.GONE) + +// Observe state changes (isSubscribed = the new state) +threadHeader.setOnThreadSubscriptionChange { isSubscribed -> + Log.d(TAG, "Thread subscribed: $isSubscribed") +} +``` + +The visibility can also be set in XML with the `app:cometchatThreadSubscriptionVisibility` attribute. + + + +```kotlin lines +CometChatThreadHeader( + parentMessage = parentMessage, + hideThreadSubscription = false, // hide the built-in bell when true + isSubscribed = null, // null = derive from the SDK's state store + onSubscriptionToggle = { isSubscribed -> + Log.d(TAG, "Thread subscribed: $isSubscribed") + }, + threadSubscriptionView = null // or your own composable replacing the bell +) +``` + + + +### Hosting the Bell in Your Own Top Bar + +Many apps (matching the CometChat sample apps and Figma) place the subscription bell in the thread screen's **top title bar** rather than the reply-count row. In Compose, the bell is available as a standalone public composable — hide the header's built-in one and host `ThreadSubscriptionBell` wherever you like: + + + +```kotlin lines +TopAppBar( + title = { Text(stringResource(R.string.thread)) }, + actions = { + if (CometChatUIKit.isThreadSubscriptionEnabled()) { + ThreadSubscriptionBell(parentMessage = parentMessage) + } + } +) + +CometChatThreadHeader( + parentMessage = parentMessage, + hideThreadSubscription = true // the bell lives in the top bar instead +) +``` + + +```kotlin lines +// Hide the kit header's bell and drive your own ImageView in the activity's title bar: +threadHeader.setThreadSubscriptionVisibility(View.GONE) + +// On tap: flip your icon optimistically, then call the SDK +CometChat.subscribeToThread(parentMessage.id, object : CometChat.CallbackListener() { + override fun onSuccess(response: String?) { } + override fun onError(e: CometChatException?) { + // revert the icon and show a toast + } +}) +``` + + + +## Behavior + +- **Optimistic with revert** — both surfaces flip instantly on tap, keep one request in flight per thread, and revert with a toast if the server rejects the change. An offline tap fails visibly and reverts; nothing is queued. +- **Auto-subscribe on reply** — sending a reply in a thread subscribes the user, and every surface flips to the subscribed state automatically. +- **Unsubscribing is not sticky** — replying again, or being @-mentioned, re-subscribes the user. +- **Unknown state renders as unsubscribed** — a message whose subscription state hasn't been learned yet (for example, one that just arrived in real time) shows the enabled subscribe control, never a spinner. + +## Cross-Surface Sync + +Both surfaces observe the UI Kit event bus, so toggling in one place updates the other without a refetch. If you build your own subscription control, emit and collect `CometChatThreadEvent` through `CometChatEvents.threadEvents` — see [Events](/ui-kit/android/events). + +## Notifications + +Whether a subscribed thread actually produces a push notification is governed by the user's notification preferences: the replies preference supports notifying only for **threads the user is subscribed to** (`SUBSCRIBE_TO_SUBSCRIBED_THREADS`). See [Thread Subscription (SDK)](/sdk/android/v5/thread-subscription#notification-preferences). + +## Next Steps & Further Reading + +- [Thread Subscription (SDK)](/sdk/android/v5/thread-subscription) — the underlying APIs, including fetching the threads a user participates in to build a thread inbox. +- [Threaded Messages Header](/ui-kit/android/threaded-messages-header) — the full component reference. +- [Message List](/ui-kit/android/message-list) — action-sheet options. diff --git a/ui-kit/android/guide-threaded-messages.mdx b/ui-kit/android/guide-threaded-messages.mdx index caeeed6f7..900ed3bd7 100644 --- a/ui-kit/android/guide-threaded-messages.mdx +++ b/ui-kit/android/guide-threaded-messages.mdx @@ -287,6 +287,9 @@ if (user.isBlockedByMe) { ## Next Steps & Further Reading + + Let users subscribe to or unsubscribe from a thread to control whether its replies notify them. + Explore this feature in the CometChat SampleApp: [GitHub → SampleApp](https://github.com/cometchat/cometchat-uikit-android/tree/v6/sample-app-kotlin) diff --git a/ui-kit/android/message-composer.mdx b/ui-kit/android/message-composer.mdx index 3ebcaa479..54fe8a119 100644 --- a/ui-kit/android/message-composer.mdx +++ b/ui-kit/android/message-composer.mdx @@ -376,6 +376,77 @@ CometChatMessageComposer( +### Rich-Text Toolbar Trailing Buttons + +Append your own buttons at the trailing end of the rich-text formatting toolbar — for snippet inserters, template pickers, AI actions, or custom styling. The UI Kit renders its own divider between the built-in formatting buttons and your content; your buttons join the toolbar's horizontal scroll and follow RTL automatically. + +Your button receives a **`ComposerInputController`** — a live handle to read and mutate the composer input: + +| Member | Description | +| --- | --- | +| `text: String` | Current plain text of the input. | +| `selection: IntRange` | Current selection; `start == end` means a collapsed caret. | +| `isCursorCollapsed: Boolean` | `true` when there is no selected range. | +| `insertAtCursor(textToInsert)` | Insert at the caret (replacing any active selection); the caret moves to the end of the insert. | +| `replaceSelection(replacement)` | Replace the selection; degrades to insert when the caret is collapsed. | +| `toggleFormat(format)` | Toggle one of the built-in `RichTextFormat` values over the selection. | +| `mentionRanges(): List` | Ranges occupied by mentions, so custom logic can skip them. | + + + + +```kotlin lines +messageComposer.setRichTextToolbarTrailingViewListener( + object : RichTextToolbarTrailingViewListener { + override fun createView( + context: Context, + user: User?, + group: Group?, + input: ComposerInputController + ): View { + return ImageButton(context).apply { + setImageResource(R.drawable.ic_snippet) + setOnClickListener { + input.insertAtCursor("Thanks for reaching out! ") + } + } + } + } +) +``` + + + + +```kotlin lines +CometChatMessageComposer( + user = user, + trailingToolbarContent = { input -> + IconButton(onClick = { + input.insertAtCursor("Thanks for reaching out! ") + }) { + Icon(painterResource(R.drawable.ic_snippet), contentDescription = "Snippet") + } + IconButton(onClick = { + input.toggleFormat(RichTextFormat.BOLD) + }) { + Icon(painterResource(R.drawable.ic_bold), contentDescription = "Bold") + } + } +) +``` + +The slot is a `RowScope` lambda, so you can emit multiple buttons. + + + + + + +The trailing section lives **inside** the rich-text toolbar — it is not rendered when the toolbar is hidden or the rich-text editor is disabled. The `ComposerInputController` is live only while the composer is mounted; don't retain it beyond your button's lifecycle. + + + ### Attachment Options Replace the default attachment options. diff --git a/ui-kit/android/message-header.mdx b/ui-kit/android/message-header.mdx index 8394ca212..744dc0b76 100644 --- a/ui-kit/android/message-header.mdx +++ b/ui-kit/android/message-header.mdx @@ -121,6 +121,38 @@ CometChatMessageHeader( +#### `onPinnedMessagesClick` (XML Views) + +The header ships a built-in **Pinned messages** menu item, hidden by default. Enable it with `setShowPinnedMessagesOption(true)` — it renders only when the Pin Message feature is enabled for the app — and handle the tap to open your [Pinned Messages](/ui-kit/android/pinned-messages) screen. + + + + +```kotlin lines +messageHeader.setShowPinnedMessagesOption(true) + +messageHeader.setOnPinnedMessagesClickListener { + // open your screen hosting CometChatPinnedMessages +} +``` + + + + +```kotlin lines +// The Compose header has no built-in menu — add a "Pinned messages" item +// to your own top bar / overflow menu, gated on the feature flag: +if (CometChatUIKit.isPinMessageEnabled()) { + DropdownMenuItem( + text = { Text("Pinned messages") }, + onClick = { /* navigate to CometChatPinnedMessages */ } + ) +} +``` + + + + #### `onError` Fires on internal errors (network failure, auth issue, SDK exception). @@ -169,6 +201,8 @@ The component listens to these SDK events internally. No manual setup needed. | `setGroup(group)` | `group = group` | Display a group's header details | | `setBackButtonVisibility(View.VISIBLE)` | `hideBackButton = false` | Toggle back button | | `setOnBackPress { }` | `onBackPress = { }` | Back button callback | +| `setShowPinnedMessagesOption(true)` | — | Show the built-in "Pinned messages" menu item (Views only; requires the Pin Message feature) | +| `setOnPinnedMessagesClickListener { }` | — | Callback for the "Pinned messages" menu item | --- diff --git a/ui-kit/android/message-list.mdx b/ui-kit/android/message-list.mdx index bf9a7bb46..57ed10af2 100644 --- a/ui-kit/android/message-list.mdx +++ b/ui-kit/android/message-list.mdx @@ -850,6 +850,17 @@ Available visibility methods (Kotlin XML): | `setTranslateMessageOptionVisibility()` | `VISIBLE` | Translate message | | `setShareMessageOptionVisibility()` | `VISIBLE` | Share message | | `setMarkAsUnreadOptionVisibility()` | `GONE` | Mark as unread | +| `setThreadSubscriptionOptionVisibility()` | `VISIBLE`* | Subscribe / Unsubscribe thread option (*renders only when the thread-subscription feature gate is on) | + +### Feature Options (Pin, Save, Thread Subscription) + +Three groups of options appear automatically when their feature is enabled for the app — no wiring needed: + +- **Pin message / Unpin message** — shown on text and media messages when `CometChatUIKit.isPinMessageEnabled()`. In groups, the option is shown only to participants with the Admin or Moderator scope, or the group owner (a client-side gate). Pinning applies immediately with a toast; unpinning asks for confirmation first. Pinned messages get a pin indicator in the bubble footer. +- **Save message / Unsave message** — shown on text and media messages when `CometChatUIKit.isSaveMessageEnabled()`, for every user. Saving applies immediately with a toast; unsaving asks for confirmation first. Saved messages get a bookmark indicator in the bubble footer. +- **Subscribe to thread / Unsubscribe from thread** — shown on regular messages (not agent or moderation-blocked ones) when thread subscription is enabled via `UIKitSettings.setEnableThreadSubscription(true)`. On a thread reply the action targets the thread's root message. Hide it with `setThreadSubscriptionOptionVisibility(View.GONE)`. + +The labels toggle with the message's current state, and if a pin/save limit is exceeded the limit toast is generated from the server response automatically. See the [Pin & Save Messages](/ui-kit/android/guide-pin-and-save-messages) and [Thread Subscription](/ui-kit/android/guide-thread-subscription) guides. ### Replacing All Options (`setOptions`) diff --git a/ui-kit/android/methods.mdx b/ui-kit/android/methods.mdx index 88c6570aa..963704528 100644 --- a/ui-kit/android/methods.mdx +++ b/ui-kit/android/methods.mdx @@ -74,6 +74,7 @@ The `UIKitSettings` is an important parameter of the `init()` function. It serve | **setAIFeatures** | `List` | Sets the AI Features that need to be added in UI Kit | | **setExtensions** | `List` | Sets the list of extension that need to be added in UI Kit | | **dateTimeFormatterCallback** | `DateTimeFormatterCallback` | Interface containing callback methods to format different types of timestamps. | +| **setEnableThreadSubscription** | `Boolean` | Opt in to the thread subscription feature. Default `false` — no subscription controls render without it. See [Thread Subscription](/ui-kit/android/guide-thread-subscription) | **Usage:** @@ -417,6 +418,25 @@ CometChatUIKit.sendCustomMessage(customMessage, object : CometChat.CallbackListe --- +### Feature Flags + +Synchronous, UI-safe checks for whether a feature is available. Use them to gate custom entry points; the built-in components already check them internally. + +| Method | Description | +| --- | --- | +| `CometChatUIKit.isPinMessageEnabled()` | Whether the Pin Message feature is enabled for the app. | +| `CometChatUIKit.isSaveMessageEnabled()` | Whether the Save Message feature is enabled for the app. | +| `CometChatUIKit.isPinConversationEnabled()` | Whether the Pin Conversation feature is enabled for the app. | +| `CometChatUIKit.isThreadSubscriptionEnabled()` | Whether thread subscription was opted into via `UIKitSettings.setEnableThreadSubscription(true)`. | + +```kotlin +if (CometChatUIKit.isPinMessageEnabled()) { + // show your "Pinned messages" entry point +} +``` + +--- + ## Next steps diff --git a/ui-kit/android/pinned-messages.mdx b/ui-kit/android/pinned-messages.mdx new file mode 100644 index 000000000..272d7bce7 --- /dev/null +++ b/ui-kit/android/pinned-messages.mdx @@ -0,0 +1,149 @@ +--- +title: "Pinned Messages" +description: "Full-screen list of all messages pinned in a conversation, with jump-to-message and unpin actions." +--- + + +```json +{ + "component": "CometChatPinnedMessages", + "package": "com.cometchat.uikit.kotlin.presentation.pinnedmessages (XML Views) / com.cometchat.uikit.compose.presentation.pinnedmessages.ui (Compose)", + "xmlElement": "", + "description": "Full-screen list of all messages pinned in a conversation, rendered as real message bubbles, with jump-to-message and long-press row actions (unpin, copy, info, delete).", + "primaryOutput": { + "messageClicked": { + "method": "setOnMessageClickListener", + "type": "(BaseMessage) -> Unit" + } + }, + "methods": { + "data": { + "setUser": { "type": "User", "note": "Scope to a one-on-one conversation. Set exactly one of setUser/setGroup." }, + "setGroup": { "type": "Group", "note": "Scope to a group conversation." } + }, + "callbacks": { + "setOnMessageClickListener": "(BaseMessage) -> Unit — row tapped; navigate to the message in its conversation", + "setOnBackClickListener": "() -> Unit — toolbar back pressed" + } + }, + "composeParams": { + "user": "User? — scope to a one-on-one conversation", + "group": "Group? — scope to a group conversation", + "onMessageClick": "(BaseMessage) -> Unit", + "onBackClick": "() -> Unit" + }, + "events": ["CometChatMessageEvent.MessagePinned", "CometChatMessageEvent.MessageUnpinned"], + "featureFlag": "CometChatUIKit.isPinMessageEnabled()" +} +``` + + + +## Where It Fits + +`CometChatPinnedMessages` is a full-screen component that lists every message pinned in a single conversation, most recently pinned first. Each row renders the actual message bubble — with the sender's avatar, name and date — so pinned media, files and text all look exactly as they do in the chat. Open it from your conversation screen (the [Message Header](/ui-kit/android/message-header) provides a built-in "Pinned messages" menu item for this), and wire `setOnMessageClickListener` to navigate back to the message in context. + +Messages are pinned and unpinned from the [Message List](/ui-kit/android/message-list) action sheet; this screen is the read view, plus a long-press menu on each row (Message info, Copy, Unpin, Subscribe/Unsubscribe to thread, Delete). + + + +Pinned messages require the **Pin Message** feature to be enabled for your app. Gate your entry point with `CometChatUIKit.isPinMessageEnabled()`. + + + +## Quick Start + + + + +Add the component to your layout XML: + +```xml activity_pinned_messages.xml lines + + + + + + +``` + +Scope it to the conversation and wire the callbacks: + +```kotlin PinnedMessagesActivity.kt lines +class PinnedMessagesActivity : AppCompatActivity() { + + private lateinit var pinnedMessages: CometChatPinnedMessages + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_pinned_messages) + + pinnedMessages = findViewById(R.id.pinned_messages) + + // Scope to the conversation — set exactly one of user / group + user?.let { pinnedMessages.setUser(it) } + group?.let { pinnedMessages.setGroup(it) } + + pinnedMessages.setOnMessageClickListener { message -> + // Navigate to the message in its conversation, + // e.g. finish with a result and call messageList.gotoMessage(message.id) + } + + pinnedMessages.setOnBackClickListener { finish() } + } +} +``` + + + + +```kotlin lines +CometChatPinnedMessages( + user = user, // or group = group — set exactly one + onMessageClick = { message -> + // Navigate to the message in its conversation + }, + onBackClick = { navController.popBackStack() } +) +``` + + + + +## Actions and Events + +### Callback Methods + +| Method (Views) / Param (Compose) | Description | +| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | +| `setOnMessageClickListener` / `onMessageClick` | Fired when a row is tapped. Receives the `BaseMessage`; navigate to it in its conversation. | +| `setOnBackClickListener` / `onBackClick` | Fired when the toolbar back button is pressed. | + +### SDK Events (Real-Time, Automatic) + +The list keeps itself up to date — when the logged-in user pins or unpins a message anywhere in the app, the row is added or removed without a refetch, via the `MessagePinned` / `MessageUnpinned` events on the UI Kit event bus; see [Events](/ui-kit/android/events). Pins made by other participants appear when the list is next opened or refetched (server-side real-time delivery for pin events is pending rollout). + +## Functionality + +- **Real bubbles** — each row hosts the message's actual bubble (text, image, video, audio, file), left-aligned with a `name • date` header and the sender's avatar. Your own messages render with the outgoing (primary-color) bubble style and the name **You**. +- **Long-press menu** — long-press a row for message actions: **Message info**, **Copy**, **Unpin**, **Subscribe/Unsubscribe to thread**, and **Delete**. Unpinning asks for confirmation before it is performed. +- **Jump to message** — tapping a row emits the message through the click callback so you can open the conversation and scroll to it. +- **Empty state** — a built-in empty state ("No pinned messages yet") is shown when the conversation has no pinned messages. +- **Read-only** — the screen never marks messages as read and does not affect unread counts or receipts. + +## ViewModel + +The screen is backed by `CometChatPinnedMessagesViewModel` (in the shared core module), which fetches via `MessagesRequestBuilder().setPinned(true)` scoped to the set user or group, applies optimistic unpin with revert-on-error, and observes the event bus for live upkeep. In Compose you can inject your own instance through the `viewModel` parameter. + +## Next Steps + +- [Message List](/ui-kit/android/message-list) — where messages are pinned and unpinned, and where the bubble pin indicator appears. +- [Message Header](/ui-kit/android/message-header) — the built-in "Pinned messages" menu entry point. +- [Saved Messages](/ui-kit/android/saved-messages) — the private, cross-conversation counterpart. +- [Pin A Message (SDK)](/sdk/android/v5/pin-message) — the underlying SDK APIs. diff --git a/ui-kit/android/saved-messages.mdx b/ui-kit/android/saved-messages.mdx new file mode 100644 index 000000000..58181f88c --- /dev/null +++ b/ui-kit/android/saved-messages.mdx @@ -0,0 +1,139 @@ +--- +title: "Saved Messages" +description: "Full-screen, private list of every message the logged-in user has saved, across all of their conversations." +--- + + +```json +{ + "component": "CometChatSavedMessages", + "package": "com.cometchat.uikit.kotlin.presentation.savedmessages (XML Views) / com.cometchat.uikit.compose.presentation.savedmessages.ui (Compose)", + "xmlElement": "", + "description": "Full-screen, private list of every message the logged-in user has saved across all conversations, with conversation-style rows, jump-to-message and long-press unsave.", + "primaryOutput": { + "messageClicked": { + "method": "setOnMessageClickListener", + "type": "(BaseMessage) -> Unit" + } + }, + "methods": { + "callbacks": { + "setOnMessageClickListener": "(BaseMessage) -> Unit — row tapped; open the source conversation at the message", + "setOnBackClickListener": "() -> Unit — toolbar back pressed" + } + }, + "composeParams": { + "onMessageClick": "(BaseMessage) -> Unit", + "onBackClick": "() -> Unit" + }, + "note": "User-level — no setUser/setGroup. Rows span all of the user's conversations.", + "events": ["CometChatMessageEvent.MessageSaved", "CometChatMessageEvent.MessageUnsaved"], + "featureFlag": "CometChatUIKit.isSaveMessageEnabled()" +} +``` + + + +## Where It Fits + +`CometChatSavedMessages` is a full-screen component that lists every message the logged-in user has bookmarked, most recently saved first. Saved messages are **private to the user** and **span all of their conversations**, so this screen is user-level: open it from your app's chrome — a profile menu, the conversations screen's user menu, or a navigation tab — not from inside a single chat. There is no `setUser`/`setGroup`; the scope is always the logged-in user. + +Messages are saved and unsaved from the [Message List](/ui-kit/android/message-list) action sheet; this screen is the read view, plus a long-press **Unsave** action on each row. + + + +Saved messages require the **Save Message** feature to be enabled for your app. Gate your entry point with `CometChatUIKit.isSaveMessageEnabled()`. + + + +## Quick Start + + + + +Add the component to your layout XML: + +```xml activity_saved_messages.xml lines + + + + + + +``` + +Wire the callbacks: + +```kotlin SavedMessagesActivity.kt lines +class SavedMessagesActivity : AppCompatActivity() { + + private lateinit var savedMessages: CometChatSavedMessages + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_saved_messages) + + savedMessages = findViewById(R.id.saved_messages) + + savedMessages.setOnMessageClickListener { message -> + // Open the source conversation at this message. Resolve the peer from + // the message: group -> message.receiver as Group; one-on-one -> the + // sender if it isn't the logged-in user, otherwise the receiver. + } + + savedMessages.setOnBackClickListener { finish() } + } +} +``` + + + + +```kotlin lines +CometChatSavedMessages( + onMessageClick = { message -> + // Open the source conversation at this message + }, + onBackClick = { navController.popBackStack() } +) +``` + + + + +## Actions and Events + +### Callback Methods + +| Method (Views) / Param (Compose) | Description | +| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | +| `setOnMessageClickListener` / `onMessageClick` | Fired when a row is tapped. Receives the `BaseMessage`; open its source conversation and scroll to the message. | +| `setOnBackClickListener` / `onBackClick` | Fired when the toolbar back button is pressed. | + +### SDK Events (Real-Time, Automatic) + +The list keeps itself up to date on this device — saving or unsaving a message anywhere in the app adds or removes the row without a refetch, via the `MessageSaved` / `MessageUnsaved` events on the UI Kit event bus; see [Events](/ui-kit/android/events). Changes made on the user's other devices appear when the list is next opened or refetched (server-side real-time delivery for save events is pending rollout). + +## Functionality + +- **Conversation-style rows** — because saved messages come from many conversations, each row shows where the message lives: the peer's or group's avatar and name, a message preview with its type icon, and the sent date. This mirrors a conversation list row (without the unread badge). +- **Unsave** — long-press a row to get the **Unsave** action; a toast confirms the result. +- **Jump to message** — tapping a row emits the message through the click callback. Use the message's receiver type and receiver to resolve which conversation to open. +- **Empty state** — a built-in empty state ("No saved messages yet") is shown when the user has not saved anything. +- **Private and read-only** — nobody else can see a user's saved messages; the screen never marks messages as read and does not affect unread counts or receipts. + +## ViewModel + +The screen is backed by `CometChatSavedMessagesViewModel` (in the shared core module), which fetches via `MessagesRequestBuilder().setSaved(true)`, applies optimistic unsave with revert-on-error, and observes the event bus for live upkeep. In Compose you can inject your own instance through the `viewModel` parameter. + +## Next Steps + +- [Message List](/ui-kit/android/message-list) — where messages are saved and unsaved, and where the bubble saved indicator appears. +- [Pinned Messages](/ui-kit/android/pinned-messages) — the conversation-wide counterpart. +- [Save A Message (SDK)](/sdk/android/v5/save-message) — the underlying SDK APIs. diff --git a/ui-kit/android/threaded-messages-header.mdx b/ui-kit/android/threaded-messages-header.mdx index c9054a2ff..089b42d7c 100644 --- a/ui-kit/android/threaded-messages-header.mdx +++ b/ui-kit/android/threaded-messages-header.mdx @@ -93,7 +93,44 @@ Prerequisites: CometChat SDK initialized with `CometChatUIKit.init()`, a user lo ### Callback Methods -`CometChatThreadHeader` is a display-only header. It does not expose component-specific callbacks like `setOnItemClick` or `setOnError`. +`CometChatThreadHeader` is a display-only header. It does not expose component-specific callbacks like `setOnItemClick` or `setOnError`. The one interactive element is the **subscription bell** (below), which reports state changes through its own callback. + +#### Subscription Bell (`onThreadSubscriptionChange` / `onSubscriptionToggle`) + +When [thread subscription](/ui-kit/android/guide-thread-subscription) is enabled (`UIKitSettings.setEnableThreadSubscription(true)`), the header renders a subscription bell as a trailing control on the reply-count bar. It flips optimistically on tap, reverts with a toast on failure, and stays in sync with the message list's Subscribe/Unsubscribe option automatically. + + + + +```kotlin lines +// Observe state changes +threadHeader.setOnThreadSubscriptionChange { isSubscribed -> + Log.d(TAG, "Thread subscribed: $isSubscribed") +} + +// Hide the bell (e.g. to host your own control in the activity's title bar) +threadHeader.setThreadSubscriptionVisibility(View.GONE) +``` + +Visibility can also be set in XML via `app:cometchatThreadSubscriptionVisibility`. + + + + +```kotlin lines +CometChatThreadHeader( + parentMessage = parentMessage, + hideThreadSubscription = false, // true to hide the built-in bell + isSubscribed = null, // null = derive from the SDK's state store + onSubscriptionToggle = { isSubscribed -> }, + threadSubscriptionView = null // or your own composable replacing the bell +) +``` + +The bell is also available standalone as the public `ThreadSubscriptionBell(parentMessage)` composable, so you can hide the header's and host it in your own top bar. + + + ### SDK Events (Real-Time, Automatic) @@ -118,6 +155,9 @@ The component listens to SDK events internally via its ViewModel. No manual setu | `setAvatarVisibility(View.GONE)` | `hideAvatar = true` | Toggle avatar visibility | | `setReceiptsVisibility(View.GONE)` | `hideReceipts = true` | Toggle read receipts | | `setReplyCountVisibility(View.GONE)` | `hideReplyCount = true` | Toggle reply count text | +| `setThreadSubscriptionVisibility(View.GONE)` | `hideThreadSubscription = true` | Toggle the subscription bell (renders only when thread subscription is enabled) | +| `setOnThreadSubscriptionChange { }` | `onSubscriptionToggle = { }` | Subscription-state change callback | +| — | `threadSubscriptionView = { }` | Replace the bell with a custom composable | --- From 39dcf3b409a9a3fec62395bc723ea484b2bcd3eb Mon Sep 17 00:00:00 2001 From: Hritika Date: Fri, 7 Aug 2026 20:22:17 +0530 Subject: [PATCH 2/2] docs(android): remove outdated notes on pagination for pinned and saved messages --- .../v5/additional-message-filtering.mdx | 6 --- sdk/android/v5/pin-message.mdx | 6 --- sdk/android/v5/save-message.mdx | 38 ------------------- 3 files changed, 50 deletions(-) diff --git a/sdk/android/v5/additional-message-filtering.mdx b/sdk/android/v5/additional-message-filtering.mdx index f903220ab..fa687c2bb 100644 --- a/sdk/android/v5/additional-message-filtering.mdx +++ b/sdk/android/v5/additional-message-filtering.mdx @@ -1363,10 +1363,4 @@ val messagesRequest = MessagesRequestBuilder() - - -The pinned and saved lists paginate forward-only on an internal server cursor. `fetchNext()` works the same from the caller's side, but message-window filters such as `setMessageId()` and `setTimestamp()` do not apply to these two queries. - - - For the full save workflow — saving, unsaving, limits and real-time events — see [Save A Message](/sdk/android/v5/save-message). diff --git a/sdk/android/v5/pin-message.mdx b/sdk/android/v5/pin-message.mdx index 56b79dace..0047bb754 100644 --- a/sdk/android/v5/pin-message.mdx +++ b/sdk/android/v5/pin-message.mdx @@ -205,12 +205,6 @@ messagesRequest.fetchNext(object : CometChat.CallbackListener> - - -The pinned-messages list paginates forward-only on an internal server cursor. `fetchNext()` works the same as any other `MessagesRequest` from the caller's side, but message-window filters such as `setMessageId()` and `setTimestamp()` do not apply to this query. See [Additional Message Filtering](/sdk/android/v5/additional-message-filtering) for all the filters of the `MessagesRequestBuilder` class. - - - ## Check if a Message is Pinned Every fetched or received message carries its pin state on the `BaseMessage` itself. diff --git a/sdk/android/v5/save-message.mdx b/sdk/android/v5/save-message.mdx index e57cabe99..0e98c4dbf 100644 --- a/sdk/android/v5/save-message.mdx +++ b/sdk/android/v5/save-message.mdx @@ -151,12 +151,6 @@ messagesRequest.fetchNext(object : CometChat.CallbackListener> - - -The saved-messages list paginates forward-only on an internal server cursor. `fetchNext()` works the same as any other `MessagesRequest` from the caller's side, but message-window filters such as `setMessageId()` and `setTimestamp()` do not apply to this query. Use each returned message's `getReceiverType()` and receiver to resolve which conversation it belongs to. See [Additional Message Filtering](/sdk/android/v5/additional-message-filtering) for all the filters of the `MessagesRequestBuilder` class. - - - If the user loses access to a conversation (for example, they are removed from a group), messages saved from it are cleaned up and no longer returned. @@ -240,38 +234,6 @@ CometChat.addMessageListener(listenerID, object : CometChat.MessageListener() { To stop listening, remove the listener with `CometChat.removeMessageListener(listenerID)`. -## Save Limit - -A user can save a limited number of messages (100 by default). When the limit is exceeded, the SDK surfaces the server error through `onError`, and the applicable limit can be read programmatically from `CometChatException.getErrorParams()` under the `limit` key — never hard-code it. - - - -```java -@Override -public void onError(CometChatException e) { - Object limit = e.getErrorParams() != null ? e.getErrorParams().get("limit") : null; - if (limit != null) { - Log.e(TAG, "You can save up to " + limit + " messages."); - } -} -``` - - - - -```kotlin -override fun onError(e: CometChatException?) { - val limit = e?.errorParams?.get("limit") - if (limit != null) { - Log.e(TAG, "You can save up to $limit messages.") - } -} -``` - - - - - ## Feature Availability Check whether the Save Message feature is enabled for your app before showing save actions in your UI. The method is synchronous and safe to call from the UI layer.