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..fa687c2bb 100644 --- a/sdk/android/v5/additional-message-filtering.mdx +++ b/sdk/android/v5/additional-message-filtering.mdx @@ -1270,3 +1270,97 @@ 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() +``` + + + + + +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..0047bb754 --- /dev/null +++ b/sdk/android/v5/pin-message.mdx @@ -0,0 +1,347 @@ +--- +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}") + } +}) +``` + + + + + +## 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..0e98c4dbf --- /dev/null +++ b/sdk/android/v5/save-message.mdx @@ -0,0 +1,260 @@ +--- +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}") + } +}) +``` + + + + + + + +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)`. + +## 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 | ---