From a1b76a5c56d50b5e611a61ada887b7720a456126 Mon Sep 17 00:00:00 2001 From: shreeyajoshi-cometchat Date: Mon, 10 Aug 2026 13:38:53 +0530 Subject: [PATCH 1/5] docs(ui-kit/ios,sdk/ios): pin & save messages, thread subscription Documents the two iOS features from the 5.1.19 release that had no documentation: Pin & Save Messages (ENG-37851..37853) and Thread Subscription (ENG-37571..37575). New pages: - sdk/ios/pin-save-message - pin/unpin, save/unsave, list fetch via MessagesRequest, and the real-time delegate callbacks - ui-kit/ios/pinned-messages - the per-conversation pinned panel - ui-kit/ios/saved-messages - the private cross-conversation panel - ui-kit/ios/guide-pin-save-message - end-to-end guide - sdk/ios/thread-subscription - subscribeToThread/unsubscribeFromThread, the tri-state threadSubscriptionState, ThreadsRequest and the MessageThread model, and the CometChatThreadDelegate callbacks - ui-kit/ios/guide-thread-subscription - the gate, the action-sheet option, the message-header bell, and cross-surface sync Updated where the features landed: message-list, message-header, message-template, events, core-features, conversations, components-overview, guide-overview, guide-threaded-messages, sdk/ios/threaded-messages and docs.json navigation. Thread-subscription notes: - copy is "Subscribe to thread"/"Unsubscribe from thread", carried by the THREAD_SUBSCRIBE/THREAD_UNSUBSCRIBE keys - the bell ships in CometChatMessageHeader, gated by parentMessage plus enableThreadSubscription, with hideThreadSubscriptionButton as the escape hatch for a host that draws its own - the action-sheet option is offered on parent messages only, never on a reply, but is not gated on reply count - unlike React, iOS has no receiver-type check, so the option appears in one-on-one conversations as well as groups; the pages say so rather than describing the feature as group-only Two SDK pages used "pinned" as a tags example, which now reads as the pin feature; changed to "important" with a pointer to the real API. Co-Authored-By: Claude Opus 5 (1M context) --- docs.json | 8 +- sdk/ios/additional-message-filtering.mdx | 6 +- sdk/ios/all-real-time-delegates-listeners.mdx | 4 + sdk/ios/pin-save-message.mdx | 308 +++++++++++ sdk/ios/send-message.mdx | 6 +- sdk/ios/thread-subscription.mdx | 203 +++++++ sdk/ios/threaded-messages.mdx | 3 + ui-kit/ios/components-overview.mdx | 2 + ui-kit/ios/core-features.mdx | 98 ++++ ui-kit/ios/events.mdx | 78 +++ ui-kit/ios/guide-overview.mdx | 1 + ui-kit/ios/guide-pin-save-message.mdx | 283 ++++++++++ ui-kit/ios/guide-thread-subscription.mdx | 148 +++++ ui-kit/ios/guide-threaded-messages.mdx | 3 + ui-kit/ios/message-header.mdx | 51 ++ ui-kit/ios/message-list.mdx | 99 ++++ ui-kit/ios/message-template.mdx | 4 + ui-kit/ios/pinned-messages.mdx | 521 ++++++++++++++++++ ui-kit/ios/saved-messages.mdx | 455 +++++++++++++++ 19 files changed, 2278 insertions(+), 3 deletions(-) create mode 100644 sdk/ios/pin-save-message.mdx create mode 100644 sdk/ios/thread-subscription.mdx create mode 100644 ui-kit/ios/guide-pin-save-message.mdx create mode 100644 ui-kit/ios/guide-thread-subscription.mdx create mode 100644 ui-kit/ios/pinned-messages.mdx create mode 100644 ui-kit/ios/saved-messages.mdx diff --git a/docs.json b/docs.json index cb43da08b..ec28bfdad 100644 --- a/docs.json +++ b/docs.json @@ -1496,6 +1496,8 @@ "ui-kit/ios/message-composer", "ui-kit/ios/compact-message-composer", "ui-kit/ios/threaded-messages-header", + "ui-kit/ios/pinned-messages", + "ui-kit/ios/saved-messages", "ui-kit/ios/incoming-call", "ui-kit/ios/outgoing-call", "ui-kit/ios/ongoing-call", @@ -1526,6 +1528,8 @@ "pages": [ "ui-kit/ios/guide-overview", "ui-kit/ios/guide-threaded-messages", + "ui-kit/ios/guide-thread-subscription", + "ui-kit/ios/guide-pin-save-message", "ui-kit/ios/guide-block-unblock-user", "ui-kit/ios/guide-new-chat", "ui-kit/ios/guide-message-privately", @@ -3846,6 +3850,7 @@ "sdk/ios/additional-message-filtering", "sdk/ios/retrieve-conversations", "sdk/ios/threaded-messages", + "sdk/ios/thread-subscription", "sdk/ios/edit-message", "sdk/ios/delete-message", "sdk/ios/delete-conversation", @@ -3854,7 +3859,8 @@ "sdk/ios/transient-messages", "sdk/ios/delivery-read-receipts", "sdk/ios/mentions", - "sdk/ios/reactions" + "sdk/ios/reactions", + "sdk/ios/pin-save-message" ] }, "sdk/ios/calling-overview", diff --git a/sdk/ios/additional-message-filtering.mdx b/sdk/ios/additional-message-filtering.mdx index d4e3f28ca..cf70e4452 100644 --- a/sdk/ios/additional-message-filtering.mdx +++ b/sdk/ios/additional-message-filtering.mdx @@ -146,13 +146,17 @@ Use `setTags()` with an array of tag names to fetch only messages with those tag ```swift var limit = 30 -let tags = ["pinned"] +let tags = ["important"] let messagesRequest = MessagesRequest.MessageRequestBuilder() .setLimit(50) .setTags(tags) .build() ``` + +To fetch pinned or saved messages, filter with `set(pinned:)` or `set(saved:)` rather than tags — see [Pin & Save Messages](/sdk/ios/pin-save-message). + + --- ## Next Steps diff --git a/sdk/ios/all-real-time-delegates-listeners.mdx b/sdk/ios/all-real-time-delegates-listeners.mdx index 5ea3143d7..efbe9fd04 100644 --- a/sdk/ios/all-real-time-delegates-listeners.mdx +++ b/sdk/ios/all-real-time-delegates-listeners.mdx @@ -212,6 +212,10 @@ The `CometChatMessageDelegate` provides you with live events related to messages | **onInteractionGoalCompleted(_ receipt: InteractionReceipt)**| This event is triggered when an interaction Goal is achieved. | | **onMessageReactionAdded(reactionEvent: ReactionEvent)** | This event is triggered when a reaction is added to a message. | | **onMessageReactionRemoved(reactionEvent: ReactionEvent)** | This event is triggered when a reaction is removed from a message. | +| **onMessagePinned(message:** [`BaseMessage`](/sdk/reference/messages#basemessage)**)** | This event is triggered when a message is pinned. Broadcast to everyone in the conversation. See [Pin & Save Messages](/sdk/ios/pin-save-message). | +| **onMessageUnpinned(message:** [`BaseMessage`](/sdk/reference/messages#basemessage)**)** | This event is triggered when a message is unpinned. Broadcast to everyone in the conversation. | +| **onMessageSaved(message:** [`BaseMessage`](/sdk/reference/messages#basemessage)**)** | This event is triggered when a message is saved. Private — reaches only the saving user's other devices. | +| **onMessageUnsaved(message:** [`BaseMessage`](/sdk/reference/messages#basemessage)**)** | This event is triggered when a message is unsaved. Private — reaches only the saving user's other devices. | | **onMessageModerated(_ message:** [`BaseMessage`](/sdk/reference/messages#basemessage)**)** | This event is triggered when a message is moderated. | | **onAIAssistantMessageReceived(_ message: AIAssistantMessage)** | This event is triggered when an AI Assistant message is received. | | **onAIToolResultMessageReceived(_ message: AIToolResultMessage)** | This event is triggered when an AI Tool result message is received. | diff --git a/sdk/ios/pin-save-message.mdx b/sdk/ios/pin-save-message.mdx new file mode 100644 index 000000000..ef3e44d76 --- /dev/null +++ b/sdk/ios/pin-save-message.mdx @@ -0,0 +1,308 @@ +--- +title: "Pin & Save Messages" +sidebarTitle: "Pin & Save Messages" +description: "Pin messages for everyone in a conversation and save messages privately using the CometChat iOS SDK, including message fields, fetching, and listener events." +--- + + + +```swift +// Pin a message for everyone in the conversation +CometChat.pinMessage(messageId: 148) { msg in } onError: { err in } +CometChat.unpinMessage(messageId: 148) { msg in } onError: { err in } + +// Save a message privately for the logged-in user +CometChat.saveMessage(messageId: 148) { msg in } onError: { err in } +CometChat.unsaveMessage(messageId: 148) { msg in } onError: { err in } + +// Presence of the timestamp IS the boolean +let isPinned = message.pinnedAt != 0 +let isSaved = message.savedAt != 0 + +// Fetch pinned messages in a conversation +MessagesRequest.MessageRequestBuilder().set(guid: "guid").set(pinned: true).set(limit: 100).build() + +// Fetch the user's saved messages across all conversations +MessagesRequest.MessageRequestBuilder().set(saved: true).set(limit: 100).build() + +// Listen for events (CometChatMessageDelegate) +func onMessagePinned(message: BaseMessage) { } +func onMessageUnpinned(message: BaseMessage) { } +func onMessageSaved(message: BaseMessage) { } +func onMessageUnsaved(message: BaseMessage) { } +``` + + +Pin and save let users mark messages for later. The two features look similar but differ in who sees the result: + +| | Pin | Save | +|---|---|---| +| Visibility | Everyone in the conversation | Private to the user | +| Scope | One conversation | All conversations | +| Permission | Group owner, admin or moderator | Anyone | +| Limit | 100 per conversation | 100 per user | + +--- + +## Pin a Message + +Pinning is conversation-wide: every participant sees the message as pinned. + + + +```swift +CometChat.pinMessage(messageId: 148) { message in + print("Pinned at: \(message.pinnedAt) by \(message.pinnedBy)") +} onError: { error in + print("Error: \(error.errorDescription)") +} +``` + + + + +Pinning is permission-gated. In a group, only the owner, admins and moderators can pin or unpin; everyone can view the pinned list. In one-to-one conversations both participants can pin. + + +--- + +## Unpin a Message + + + +```swift +CometChat.unpinMessage(messageId: 148) { message in + // pinnedAt is cleared back to 0 + print("Unpinned: \(message.pinnedAt == 0)") +} onError: { error in + print("Error: \(error.errorDescription)") +} +``` + + + +--- + +## Save a Message + +Saving is private to the logged-in user and syncs across that user's own devices. It carries no permission checks. + + + +```swift +CometChat.saveMessage(messageId: 148) { message in + print("Saved at: \(message.savedAt)") +} onError: { error in + print("Error: \(error.errorDescription)") +} +``` + + + +--- + +## Unsave a Message + + + +```swift +CometChat.unsaveMessage(messageId: 148) { message in + print("Unsaved: \(message.savedAt == 0)") +} onError: { error in + print("Error: \(error.errorDescription)") +} +``` + + + +All four methods return the updated [`BaseMessage`](/sdk/reference/messages#basemessage). + +--- + +## Message Fields + +Three fields on `BaseMessage` carry the state: + +| Field | Type | Scope | Description | +|-------|------|-------|-------------| +| `pinnedAt` | `Double` | Global | When the message was pinned. `0` when not pinned. | +| `pinnedBy` | `String` | Global | UID of the last user to pin it. `app_system` for a system pin. | +| `savedAt` | `Double` | Per-viewer | When the logged-in user saved it. `0` when not saved. | + + +**Presence of the timestamp is the boolean.** There is no separate `isPinned` flag — check `pinnedAt != 0` and `savedAt != 0`. Unpinning and unsaving clear the field back to `0` rather than leaving a stale value. + +`savedAt` is per-viewer: it is only ever populated for the user who saved the message, so you cannot use it to tell whether someone else saved it. + + + + +```swift +let isPinned = message.pinnedAt != 0 +let isSaved = message.savedAt != 0 + +// A pin applied by an admin from the dashboard rather than a user +let isSystemPin = message.pinnedBy == "app_system" +``` + + + +--- + +## Fetch Pinned Messages + +Pinned messages are fetched with `MessagesRequest` filtered by `pinned`, scoped to a conversation with `uid` or `guid`. + + + +```swift +let request = MessagesRequest.MessageRequestBuilder() + .set(guid: "cometchat-guid-1") + .set(pinned: true) + .set(limit: 100) + .build() + +request.fetchPrevious { messages in + print("Pinned: \(messages?.count ?? 0)") +} onError: { error in + print("Error: \(error?.errorDescription)") +} +``` + + +```swift +let request = MessagesRequest.MessageRequestBuilder() + .set(uid: "cometchat-uid-1") + .set(pinned: true) + .set(limit: 100) + .build() + +request.fetchPrevious { messages in + print("Pinned: \(messages?.count ?? 0)") +} onError: { error in + print("Error: \(error?.errorDescription)") +} +``` + + + +--- + +## Fetch Saved Messages + +Saved messages are a per-user collection spanning every conversation, so the request takes **no** `uid` or `guid`. + + + +```swift +let request = MessagesRequest.MessageRequestBuilder() + .set(saved: true) + .set(limit: 100) + .build() + +request.fetchPrevious { messages in + // Each message carries receiverUid / receiverType identifying its source conversation + print("Saved: \(messages?.count ?? 0)") +} onError: { error in + print("Error: \(error?.errorDescription)") +} +``` + + + + +Both limits are capped at 100 server-side, so a single page fetches the whole list. + + +--- + +## Real-time Events + +Implement `CometChatMessageDelegate` to receive pin and save updates. All four callbacks are optional and each carries the full updated message. + + + +```swift +extension ViewController: CometChatMessageDelegate { + + func onMessagePinned(message: BaseMessage) { + // Broadcast to everyone in the conversation + print("Pinned by \(message.pinnedBy)") + } + + func onMessageUnpinned(message: BaseMessage) { + print("Unpinned: \(message.id)") + } + + func onMessageSaved(message: BaseMessage) { + // Private — only reaches the saving user's other devices + print("Saved: \(message.id)") + } + + func onMessageUnsaved(message: BaseMessage) { + print("Unsaved: \(message.id)") + } +} +``` + + + +Register the delegate as you would for any message event — see [Real-time Delegates and Listeners](/sdk/ios/all-real-time-delegates-listeners). + +Pin events are broadcast to every participant. Save events are private and reach only the saving user's other devices. + +--- + +## Feature Availability + +Both features are gated per app and can be checked before showing any UI: + + + +```swift +if CometChat.isPinMessageEnabled() { + // Show pin options +} + +if CometChat.isSaveMessageEnabled() { + // Show save options +} +``` + + + +--- + +## Error Handling + +| Error code | Meaning | +|------------|---------| +| `ERR_PINNED_MESSAGES_LIMIT_EXCEEDED` | The conversation already has the maximum pinned messages. | +| `ERR_SAVED_MESSAGES_LIMIT_EXCEEDED` | The user already has the maximum saved messages. | +| `ERR_PERMISSION_DENIED` | The user's scope does not allow pinning here. | +| `ERR_MESSAGE_NO_ACCESS` | The user cannot access this message. | +| `ERR_MESSAGE_ACTION_NOT_ALLOWED` | The action is not allowed on this message. | +| `ERR_FEATURE_NOT_ACCESSIBLE` | The feature is not enabled for this app. | + +For the two limit errors, read the actual cap from the exception's `errorParams` rather than hard-coding a number — the limit is server-owned and may change. + +--- + +## Edge Cases + +| Scenario | Behavior | +|----------|----------| +| A pinned message is deleted | Automatically unpinned; no placeholder is left behind. | +| A pinned message is edited | Keeps its pin. | +| A saved message becomes inaccessible | Dropped from subsequent saved fetches. | +| A thread reply | Can be pinned and saved. Pinned lists return the parent message for context. | +| Someone else unpins your pin | Allowed — any user with permission can unpin. | + +--- + +## Related + +- [Pinned Messages UI Component](/ui-kit/ios/pinned-messages) +- [Saved Messages UI Component](/ui-kit/ios/saved-messages) +- [Additional Message Filtering](/sdk/ios/additional-message-filtering) +- [Real-time Delegates and Listeners](/sdk/ios/all-real-time-delegates-listeners) diff --git a/sdk/ios/send-message.mdx b/sdk/ios/send-message.mdx index 9df4db075..7cce7a243 100644 --- a/sdk/ios/send-message.mdx +++ b/sdk/ios/send-message.mdx @@ -122,10 +122,14 @@ NSDictionary * metadata = @{@"latitude":@"50.6192171633316",@"longitude":@"-72.6 ### Add Tags ```swift -let tags = ["pinned"] +let tags = ["important"] textMessage.tags = tags ``` + +Tags are arbitrary labels of your own. To pin or save a message, use the dedicated APIs instead — see [Pin & Save Messages](/sdk/ios/pin-save-message). + + ### Quote a Message ```swift diff --git a/sdk/ios/thread-subscription.mdx b/sdk/ios/thread-subscription.mdx new file mode 100644 index 000000000..3083911b2 --- /dev/null +++ b/sdk/ios/thread-subscription.mdx @@ -0,0 +1,203 @@ +--- +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 iOS SDK. + + + +Thread subscription builds on [Threaded Messages](/sdk/ios/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. + + + +```swift +let parentMessageId = 1 + +CometChat.subscribeToThread(parentMessageId: parentMessageId) { response in + print("Subscribed to thread: \(response)") +} onError: { error in + print("Failed to subscribe: \(error.errorDescription)") +} +``` + + + +## 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. + + + +```swift +let parentMessageId = 1 + +CometChat.unsubscribeFromThread(parentMessageId: parentMessageId) { response in + print("Unsubscribed from thread: \(response)") +} onError: { error in + print("Failed to unsubscribe: \(error.errorDescription)") +} +``` + + + + + +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 + +`threadSubscriptionState(forParentMessageId:)` 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. + + + +```swift +switch CometChat.threadSubscriptionState(forParentMessageId: parentMessageId) { +case .SUBSCRIBED: break // render "Unsubscribe from thread" +case .NOT_SUBSCRIBED: break // render "Subscribe to thread" +case .UNKNOWN: break // render "Subscribe to thread" +@unknown default: break +} +``` + + + +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. + + + +## 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 | +| ----------------------------- | -------------------------------------------------------------------------------------------- | +| `set(limit:)` | Page size. Thread rows are heavy — each carries a root message and a last reply. | +| `set(uid:)` | Scope the list to threads in the one-on-one conversation with this user. Mutually exclusive with `set(guid:)`. | +| `set(guid:)` | Scope the list to threads in this group. Mutually exclusive with `set(uid:)`. | +| `set(participatedByMe:)` | Defaults to `true`. Only the threads the logged-in user participates in are returned. | + + + +```swift +let threadsRequest = ThreadsRequest.ThreadsRequestBuilder() + .set(limit: 30) + .build() + +threadsRequest.fetchNext { threads in + for thread in threads { + print("Thread \(thread.parentMessageId) has \(thread.replyCount) replies") + } +} onError: { error in + print("Threads fetch failed: \(error.errorDescription)") +} +``` + + + +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. + +### The MessageThread Model + +Each row is a `MessageThread`: + +| Property | Description | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `parentMessageId` | The thread's identity — the ID of its root message. | +| `parentMessage` | The root message as a full `BaseMessage`. | +| `replyCount` | Number of replies in the thread. | +| `lastReply` | The most recent reply as a `BaseMessage`. `nil` for a thread with no replies yet — expected, not an error. | +| `conversationId` | The ID of the conversation the thread belongs to. | +| `receiverType` | `user` or `group`. | +| `receiverUid` | The raw `UID`/`GUID` of the conversation. Resolve the display name and avatar yourself via `CometChat.getUser()` / `CometChat.getGroup()`. | +| `subscriptionState` | Always `SUBSCRIBED` for rows in this list. | +| `unreadReplyCount` | Reserved for future use — currently `nil` (unknown), which is not the same as `0`. | +| `updatedAt` | An internal pagination cursor. **Do not sort your UI on it.** | + + + +To order rows in your UI, sort on `lastReply?.sentAt`, falling back to `parentMessage?.sentAt` for zero-reply threads — not on `updatedAt`. + + + + + +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 + +Conform to `CometChatThreadDelegate` to keep your UI in sync as subscription state changes and replies arrive. + + + +```swift +CometChat.threadDelegate = self + +extension ViewController: CometChatThreadDelegate { + + func onThreadSubscriptionChanged(event: ThreadSubscriptionEvent) { + print("Thread \(event.parentMessageId) is now \(event.subscriptionState)") + } + + func onThreadReplyReceived(event: ThreadReplyEvent) { + print("New reply in thread \(event.parentMessageId): \(event.reply.id)") + } +} +``` + + + +Both callbacks are optional. The events carry: + +| Event | Properties | +| ------------------------- | --------------------------------------------------------------------- | +| `ThreadSubscriptionEvent` | `parentMessageId`, `subscriptionState`, `source` | +| `ThreadReplyEvent` | `parentMessageId`, `reply`, `conversationId`, `source` | + +- `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. + + + +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 replies options. + +| 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/ios/threaded-messages.mdx b/sdk/ios/threaded-messages.mdx index 2b609da8f..d49e601d8 100644 --- a/sdk/ios/threaded-messages.mdx +++ b/sdk/ios/threaded-messages.mdx @@ -177,6 +177,9 @@ messagesRequest.fetchPrevious(onSuccess: { (messages) in ## Next Steps + + Subscribe to or unsubscribe from a thread, and fetch the threads a user participates in + Send text, media, and custom messages to users and groups diff --git a/ui-kit/ios/components-overview.mdx b/ui-kit/ios/components-overview.mdx index 06fa3c5b1..d76c21f36 100644 --- a/ui-kit/ios/components-overview.mdx +++ b/ui-kit/ios/components-overview.mdx @@ -96,6 +96,8 @@ UI elements with built-in business logic. They fetch data, handle actions, and e | `CometChatMessageComposer` | Message input | | `CometChatMessageHeader` | Chat header | | `CometChatCallLogs` | Call history | +| `CometChatPinnedMessages` | Pinned messages in one conversation | +| `CometChatSavedMessages` | The logged-in user's saved messages, across all conversations | diff --git a/ui-kit/ios/core-features.mdx b/ui-kit/ios/core-features.mdx index ae494ff0d..615977623 100644 --- a/ui-kit/ios/core-features.mdx +++ b/ui-kit/ios/core-features.mdx @@ -16,6 +16,7 @@ description: "Review CometChat iOS UI Kit core features for messaging, media sha {"name": "reactions", "description": "Let users react to messages with emojis", "component": "CometChatMessageList", "enabledByDefault": true}, {"name": "mentions", "description": "Tag users in messages with @mentions", "component": "CometChatMessageComposer", "enabledByDefault": true}, {"name": "threadedConversations", "description": "Reply to specific messages in threads", "component": "CometChatThreadedMessageHeader", "enabledByDefault": true}, + {"name": "threadSubscription", "description": "Subscribe to or unsubscribe from a thread's replies", "component": "CometChatMessageList", "enabledByDefault": false}, {"name": "groupChat", "description": "Create and manage group conversations", "component": "CometChatGroups", "enabledByDefault": true}, {"name": "search", "description": "Search across conversations and messages", "component": "CometChatSearch", "enabledByDefault": true} ], @@ -560,6 +561,40 @@ class ThreadedMessagesViewController: UIViewController { **Component Used:** - [ThreadedMessages](/ui-kit/ios/threaded-messages-header) - Displays thread replies +## Thread Subscription + +Let users subscribe to a thread to be notified about its replies, or unsubscribe to mute it. Users are subscribed automatically when they start a thread, reply in one, or are @-mentioned in one — subscribing explicitly is how they opt in to a thread they haven't participated in yet. + +```swift lines +import UIKit +import CometChatUIKitSwift +import CometChatSDK + +class ThreadSubscriptionViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + let messageListView = CometChatMessageList() + + // Off by default — opt in. Adds a "Subscribe to thread" / + // "Unsubscribe from thread" option to the message action sheet, + // on parent messages only. + messageListView.enableThreadSubscription = true + } + + // Read the current state synchronously — no network call + func isSubscribed(parentMessageId: Int) -> Bool { + CometChat.threadSubscriptionState(forParentMessageId: parentMessageId) == .SUBSCRIBED + } +} +``` + +**Component Used:** +- [MessageList](/ui-kit/ios/message-list) - Hosts the subscribe/unsubscribe option + +See the [Thread Subscription guide](/ui-kit/ios/guide-thread-subscription) for the full feature. + ## Group Chat Create and manage group conversations. @@ -672,6 +707,69 @@ class QuotedReplyViewController: UIViewController { } ``` +## Pin Message + +Pin an important message so everyone in the conversation can find it. Pinning is restricted to group owners, admins and moderators; everyone else sees the pin indicator and the pinned list. + +```swift lines +import UIKit +import CometChatUIKitSwift +import CometChatSDK + +class PinMessageViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + let messageListView = CometChatMessageList() + + // Pin is off by default — opt in, and the feature must also be + // enabled for your app (CometChat.isPinMessageEnabled()). + messageListView.enablePinMessage = true + + // Open the per-conversation pinned list + let pinnedVC = CometChatPinnedMessages(group: group) + pinnedVC.set(onMessageClicked: { [weak self] message in + self?.navigationController?.popViewController(animated: true) + messageListView.goToMessage(withId: message.id) + }) + navigationController?.pushViewController(pinnedVC, animated: true) + } +} +``` + +See the [Pin & Save Messages guide](/ui-kit/ios/guide-pin-save-message) and [Pinned Messages](/ui-kit/ios/pinned-messages). + +## Save Message + +Save a message privately for later. Saves are visible only to the user who made them and span every conversation, so they are listed on a user-level screen rather than a per-conversation one. + +```swift lines +import UIKit +import CometChatUIKitSwift +import CometChatSDK + +class SaveMessageViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + let messageListView = CometChatMessageList() + + // Save is off by default — opt in, and the feature must also be + // enabled for your app (CometChat.isSaveMessageEnabled()). + messageListView.enableSaveMessage = true + + // Saved messages span every conversation, so open this from app chrome + let savedVC = CometChatSavedMessages() + savedVC.hideNavigationBar = false + navigationController?.pushViewController(savedVC, animated: true) + } +} +``` + +See the [Pin & Save Messages guide](/ui-kit/ios/guide-pin-save-message) and [Saved Messages](/ui-kit/ios/saved-messages). + ## Search Search across conversations and messages. diff --git a/ui-kit/ios/events.mdx b/ui-kit/ios/events.mdx index 4be764cd2..9775981b2 100644 --- a/ui-kit/ios/events.mdx +++ b/ui-kit/ios/events.mdx @@ -360,6 +360,64 @@ CometChatConversationEvents.ccUpdateConversation(conversation: updatedConversati | `ccConversationDeleted` | Conversation was deleted | | `ccUpdateConversation` | Conversation was updated | +## Thread Events + +Listen for thread subscription changes so any subscription control you host stays in agreement with the kit's action-sheet option, without a refetch. + +```swift lines +import UIKit +import CometChatUIKitSwift +import CometChatSDK + +class ThreadedMessagesVC: UIViewController { + + var parentMessage: BaseMessage? + + // Listeners are keyed by id, and registering a duplicate id evicts the previous + // listener — so use a distinct id per screen. + private let threadListenerID = "threaded-messages-vc-\(UUID().uuidString)" + + override func viewDidLoad() { + super.viewDidLoad() + CometChatThreadEvents.addListener(threadListenerID, self) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + CometChatThreadEvents.removeListener(threadListenerID) + } +} + +extension ThreadedMessagesVC: CometChatThreadEventListener { + + func ccThreadSubscriptionChanged(parentMessageId: Int, isSubscribed: Bool) { + // parentMessageId identifies the thread, not the message the tap came from + guard parentMessageId == parentMessage?.id else { return } + print(isSubscribed ? "Subscribed to thread" : "Unsubscribed from thread") + } +} +``` + +### Emit Thread Events + +```swift lines +// The local user subscribed to or unsubscribed from a thread +CometChatThreadEvents.ccThreadSubscriptionChanged(parentMessageId: parentMessageId, + isSubscribed: true) +``` + + + +The kit emits this **only on success**, so every surface keeps showing the state the server still holds if a toggle fails. + + + +### Thread Events Reference + +| Event | Description | +|-------|-------------| +| `ccThreadSubscriptionChanged` | The local user subscribed to or unsubscribed from a thread. `parentMessageId` identifies the thread; `isSubscribed` is the new state. | + ## Message Events Listen for message-related actions like send, edit, delete, and reactions. @@ -454,6 +512,16 @@ extension MessageEventsViewController: CometChatMessageEventListener { print("Group video call: \(group.name ?? "")") } + // Pin & Save + func ccMessagePinned(message: BaseMessage, status: MessageStatus) { + // There is no separate unpinned callback — read the field to tell which happened + print(message.pinnedAt != 0 ? "Pinned" : "Unpinned") + } + + func ccMessageSaved(message: BaseMessage, status: MessageStatus) { + print(message.savedAt != 0 ? "Saved" : "Unsaved") + } + // Errors func onMessageError(error: CometChatException) { print("Error: \(error.errorDescription)") @@ -484,6 +552,12 @@ CometChatMessageEvents.emitOnViewInformation(user: selectedUser) // Thread parent updated CometChatMessageEvents.emitOnParentMessageUpdate(message: parentMessage) + +// Message pinned or unpinned by the local user +CometChatMessageEvents.ccMessagePinned(message: pinnedMessage, status: .success) + +// Message saved or unsaved by the local user +CometChatMessageEvents.ccMessageSaved(message: savedMessage, status: .success) ``` ### Message Events Reference @@ -498,6 +572,8 @@ CometChatMessageEvents.emitOnParentMessageUpdate(message: parentMessage) | `onMessageReact` | Reaction added to message | | `onLiveReaction` | Live reaction sent | | `onParentMessageUpdate` | Thread parent message updated | +| `ccMessagePinned` | The local user pinned or unpinned a message. Read `message.pinnedAt` to tell which — non-zero means pinned. | +| `ccMessageSaved` | The local user saved or unsaved a message. Read `message.savedAt` to tell which — non-zero means saved. | | `onViewInformation` | Info button tapped (user or group) | | `onVoiceCall` | Voice call initiated | | `onVideoCall` | Video call initiated | @@ -745,6 +821,8 @@ extension ChatEventManager: CometChatMessageEventListener { func onVideoCall(group: Group) {} func onMessageError(error: CometChatException) {} func onMessageReply(message: BaseMessage, status: MessageStatus) {} + func ccMessagePinned(message: BaseMessage, status: MessageStatus) {} + func ccMessageSaved(message: BaseMessage, status: MessageStatus) {} } // MARK: - Call Events diff --git a/ui-kit/ios/guide-overview.mdx b/ui-kit/ios/guide-overview.mdx index 0a203929d..58e3d4381 100644 --- a/ui-kit/ios/guide-overview.mdx +++ b/ui-kit/ios/guide-overview.mdx @@ -29,6 +29,7 @@ Use these guides after completing [Getting Started](/ui-kit/ios/getting-started) | [Group Ownership Transfer](/ui-kit/ios/guide-group-ownership) | Allow admins to hand over group ownership securely to another member. | | [Message Privately](/ui-kit/ios/guide-message-privately) | Launch a direct 1:1 conversation from a group chat; streamlines side discussions without manual user searches. | | [New Chat](/ui-kit/ios/guide-new-chat) | Offer a unified entry to discover users and groups and jump into new conversations quickly. | +| [Pin & Save Messages](/ui-kit/ios/guide-pin-save-message) | Pin messages for everyone in a conversation and let users save messages privately, with role gating and dedicated list screens. | | [Threaded Messages](/ui-kit/ios/guide-threaded-messages) | Add threaded reply views so users can branch focused discussions off individual messages. | ## Related Resources diff --git a/ui-kit/ios/guide-pin-save-message.mdx b/ui-kit/ios/guide-pin-save-message.mdx new file mode 100644 index 000000000..9b1ad8dbb --- /dev/null +++ b/ui-kit/ios/guide-pin-save-message.mdx @@ -0,0 +1,283 @@ +--- +title: "Pin & Save Messages" +sidebarTitle: "Pin & Save Messages" +description: "Enable pin and save messages in the CometChat iOS UI Kit, wire the pinned and saved screens, gate pinning by role, and handle limit and permission errors." +--- + +Let users mark messages for later: **pin** a message so everyone in the conversation sees it, or **save** one privately for yourself. + +## Overview + +Pin and save look alike in the action sheet but behave differently: + +| | Pin | Save | +|---|---|---| +| Who sees it | Everyone in the conversation | Only you | +| Scope | One conversation | All conversations | +| Who can do it | Group owner, admin or moderator | Anyone | +| Limit | 100 per conversation | 100 per user | +| Confirmation | Asks first — it is public | Applies immediately | + +With the iOS UI Kit you get: the action-sheet options, pin and bookmark indicators on the bubble, and two full-screen list surfaces. + +## Prerequisites + +1. Completed [Getting Started](/ui-kit/ios/getting-started) setup +2. CometChat UIKit v5+ installed +3. User logged in with `CometChatUIKit.login()` +4. Pin and save enabled for your app + +## Components + +| Component | Description | +|-----------|-------------| +| `CometChatMessageList` | Hosts the pin/save options and the bubble indicators | +| `CometChatPinnedMessages` | Per-conversation list of pinned messages | +| `CometChatSavedMessages` | User-level list of saved messages | +| `CometChatMessageEvents` | Emits pin and save events to the rest of your app | + +## Integration Steps + +### Step 1: Turn the Features On + +Both features are **off by default**. Nothing appears in the action sheet until you opt in. + +```swift lines +import UIKit +import CometChatUIKitSwift +import CometChatSDK + +let messageListView = CometChatMessageList() +messageListView.set(user: user) + +// Opt in — both default to false +messageListView.enablePinMessage = true +messageListView.enableSaveMessage = true +``` + + +Two switches must both be on. `enablePinMessage` is your app-side opt-in; the feature must **also** be enabled for your CometChat app, which the kit checks via `CometChat.isPinMessageEnabled()`. If the options do not appear, this is almost always why. + + +You can verify the server-side flag directly: + +```swift lines +if CometChat.isPinMessageEnabled() { + messageListView.enablePinMessage = true +} + +if CometChat.isSaveMessageEnabled() { + messageListView.enableSaveMessage = true +} +``` + +### Step 2: Find the Options + +Long-press a message to open the action sheet. Pin and save sit behind a **"More…"** row, which keeps the sheet from overflowing on smaller screens. Tapping it swaps the list, and tapping it again goes back. + +To change which options live behind that row, edit `MessageOptionConstants.overflowOptionIds`: + +```swift lines +// Show pin inline and keep only save behind "More…" +MessageOptionConstants.overflowOptionIds = [ + MessageOptionConstants.saveMessage, + MessageOptionConstants.unsaveMessage +] + +// Or show everything inline +MessageOptionConstants.overflowOptionIds = [] +``` + +Pinning shows a confirmation first, because it changes what everyone in the conversation sees. Saving applies immediately — it is private and one tap to undo. + +### Step 3: Add the Pinned Messages Screen + +Pinned messages belong to one conversation, so open this from that conversation — typically the message header's overflow menu. + +```swift lines +private func openPinnedMessages() { + let pinnedVC = CometChatPinnedMessages(user: user, group: group) + + // Tapping a row returns here and jumps to the message. + pinnedVC.set(onMessageClicked: { [weak self] message in + guard let self = self else { return } + self.navigationController?.popViewController(animated: true) + self.messageListView.goToMessage(withId: message.id) + }) + + navigationController?.pushViewController(pinnedVC, animated: true) +} +``` + +See [Pinned Messages](/ui-kit/ios/pinned-messages) for the full component reference. + +### Step 4: Add the Saved Messages Screen + +Saved messages span every conversation, so this one belongs in your app's chrome — a tab, the chats-screen menu, or a profile entry. It takes no user or group. + +```swift lines +private func openSavedMessages() { + let savedVC = CometChatSavedMessages() + savedVC.hidesBottomBarWhenPushed = true + + // The component supplies its own back chevron, so leave hideBackButton alone. + savedVC.hideNavigationBar = false + + savedVC.set(onBack: { [weak self] in + self?.navigationController?.setNavigationBarHidden(true, animated: true) + self?.navigationController?.popViewController(animated: true) + }) + + savedVC.set(onMessageClicked: { [weak self] message in + self?.openConversation(for: message) + }) + + navigationController?.setNavigationBarHidden(false, animated: true) + navigationController?.pushViewController(savedVC, animated: true) +} +``` + + +If you push this screen from a tab that hides the navigation bar, set `hideNavigationBar = false` or the title and back button never appear. Do **not** also set `hideBackButton = false` — the component supplies its own chevron, so clearing that flag renders two back buttons. + + +Because a saved row can come from any chat, `onMessageClicked` has to resolve the conversation — see [Saved Messages](/ui-kit/ios/saved-messages) for that snippet. + +### Step 5: Handle Errors + +Failures roll the change back automatically and show a message. To react yourself, listen for the error codes on `PinSaveErrorCodes`: + +```swift lines +switch error.errorCode { +case PinSaveErrorCodes.pinLimitReached: + // Read the real cap from the server rather than hard-coding it + let limit = error.errorParams?["limit"] as? Int + print("Pin limit reached: \(limit ?? 0)") +case PinSaveErrorCodes.pinPermissionDenied: + print("This user cannot pin here") +default: + break +} +``` + + +Always read the cap from `errorParams["limit"]`. The limit is server-owned and hard-coding it means your copy goes stale the moment it changes. + + +## Customization Options + +### Who Can Pin + +Pinning is gated by role, not authorship — a moderator can pin someone else's message, and a participant cannot pin their own. The kit uses `GroupMembersUtils.allowPinMessage(group:)`: + +| Scope | Can pin | +|-------|---------| +| Group owner | Yes | +| Admin | Yes | +| Moderator | Yes | +| Participant | No | +| One-to-one (`nil` group) | Yes | + +Saving has no role gate — anyone can save anything they can see. + +Use the same check to hide the unpin action for users who cannot pin: + +```swift lines +pinnedVC.set(hideUnpinOption: !GroupMembersUtils.allowPinMessage(group: group)) +``` + +### Hiding Individual Options + +`enable*` turns the feature on; `hide*` suppresses one option independently. + +```swift lines +// Feature on, but no save option in this screen's action sheet +messageListView.enablePinMessage = true +messageListView.enableSaveMessage = true +messageListView.hideSaveMessageOption = true +``` + +### Styling + +Both screens take their own style object built from theme tokens: + +```swift lines +CometChatPinnedMessages.style.unpinActionBackgroundColor = CometChatTheme.errorColor +CometChatSavedMessages.style.messageTypeImageTint = CometChatTheme.iconColorHighlight +``` + +### Reacting to Pin and Save Elsewhere + +Conform to `CometChatMessageEventListener` to update your own UI when the local user pins or saves: + +```swift lines +extension MyViewController: CometChatMessageEventListener { + + func ccMessagePinned(message: BaseMessage, status: MessageStatus) { + // No separate unpinned event — read the field to tell which happened + let isPinned = message.pinnedAt != 0 + print(isPinned ? "Pinned" : "Unpinned") + } + + func ccMessageSaved(message: BaseMessage, status: MessageStatus) { + let isSaved = message.savedAt != 0 + print(isSaved ? "Saved" : "Unsaved") + } +} +``` + +All listener methods have default empty implementations, so you only implement the ones you need. See [Events](/ui-kit/ios/events). + +## Edge Cases + +| Scenario | Handling | +|----------|----------| +| Action messages (group joins, calls) | No pin or save options — they are system chrome | +| Deleted message | Options hidden; a pinned message that is deleted is auto-unpinned | +| Message still sending | No options until the server assigns an id | +| Moderation pending | No options until the message is approved | +| Thread replies | Pinnable and savable; the pinned list shows the parent for context | +| Edited message | Keeps its pin | +| Participant in a group | Sees the pinned list and indicators, but no pin option | + +## Error Handling + +| Error code | Solution | +|------------|----------| +| `ERR_PINNED_MESSAGES_LIMIT_EXCEEDED` | Prompt the user to unpin something. Read the cap from `errorParams["limit"]`. | +| `ERR_SAVED_MESSAGES_LIMIT_EXCEEDED` | Prompt the user to unsave something. | +| `ERR_PERMISSION_DENIED` | The user's scope does not allow pinning; hide the option for them. | +| `ERR_MESSAGE_NO_ACCESS` | The user can no longer access the message. | +| `ERR_MESSAGE_ACTION_NOT_ALLOWED` | The action is not allowed on this message. | +| `ERR_FEATURE_NOT_ACCESSIBLE` | The feature is not enabled for your app. | + +## Feature Matrix + +| Feature | Implementation | +|---------|----------------| +| Enable pin | `CometChatMessageList.enablePinMessage` | +| Enable save | `CometChatMessageList.enableSaveMessage` | +| Hide an option | `hidePinMessageOption` / `hideSaveMessageOption` | +| Overflow row contents | `MessageOptionConstants.overflowOptionIds` | +| Role gate | `GroupMembersUtils.allowPinMessage(group:)` | +| Pinned list | `CometChatPinnedMessages(user:group:)` | +| Saved list | `CometChatSavedMessages()` | +| Jump to a message | `CometChatMessageList.goToMessage(withId:)` | +| Error codes | `PinSaveErrorCodes` | +| Events | `ccMessagePinned` / `ccMessageSaved` | + +## Related Components + +- [Pinned Messages](/ui-kit/ios/pinned-messages) - Per-conversation pinned list +- [Saved Messages](/ui-kit/ios/saved-messages) - User-level saved list +- [Message List](/ui-kit/ios/message-list) - Hosts the options and indicators +- [Events](/ui-kit/ios/events) - Pin and save event callbacks + + + + The underlying SDK methods and message fields + + + Overview of messaging features + + diff --git a/ui-kit/ios/guide-thread-subscription.mdx b/ui-kit/ios/guide-thread-subscription.mdx new file mode 100644 index 000000000..636409445 --- /dev/null +++ b/ui-kit/ios/guide-thread-subscription.mdx @@ -0,0 +1,148 @@ +--- +title: "Thread Subscription" +sidebarTitle: "Thread Subscription" +description: "Let users subscribe to or unsubscribe from message threads in the CometChat iOS UI Kit 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 the toggle as a **Subscribe to thread / Unsubscribe from thread** option in the message action sheet, and broadcasts every change on the event bus so any control you host elsewhere — such as a button in your thread screen's title bar — stays in sync. + +## Prerequisites + +- Threaded messages working in your app — see [Threaded Messages](/ui-kit/ios/guide-threaded-messages). +- CometChat UI Kit for iOS with Chat SDK v5 or later. + +## Enable the Feature + +Thread subscription is **off by default**. Opt in per [CometChatMessageList](/ui-kit/ios/message-list) instance. When the gate is off, the option never renders and no subscription request is ever made. + +```swift +let messageListView = CometChatMessageList() +messageListView.enableThreadSubscription = true // opt in — default is false +``` + +Set it on **both** your main message list and the message list inside your thread screen, so the option is available in either place. + +## Surface 1: The Message Action Sheet Option + +With the gate on, `CometChatMessageList` adds a **Subscribe to thread** / **Unsubscribe from thread** option to the long-press action sheet. The label and icon reflect the current state, read synchronously from the SDK when the sheet is built. + +The option appears only on **parent messages** — never on a reply inside a thread: + +```swift +// The kit's gate, for reference: +// enableThreadSubscription && parentMessageId == 0 && !hideThreadSubscriptionOption +``` + + + +A subscription is always rooted at the thread's parent message. Offering the option on a reply would create a thread row the user can never open, so the kit hides it there entirely. It is **not** gated on reply count — subscribing to a message with no replies yet is the point. + + + + + +Unlike some other CometChat platforms, iOS offers thread subscription in **one-on-one conversations as well as groups** — there is no receiver-type check. + + + +To hide the option while keeping the rest of the feature: + +```swift +messageListView.hideThreadSubscriptionOption = true +``` + +To replace the kit's behavior with your own, supply an `onItemClick` on a custom option with the id `MessageOptionConstants.threadSubscription` — the kit calls your handler instead of its own. + +## Surface 2: The Bell in the Message Header + +[CometChatMessageHeader](/ui-kit/ios/message-header) renders a subscribe/unsubscribe bell in its trailing area. Set `parentMessage` to put the header in **thread mode** — the bell renders only then, so a conversation header is unaffected. + +```swift +let messageHeaderView = CometChatMessageHeader() +messageHeaderView.set(parentMessage: parentMessage) // puts the header in thread mode +messageHeaderView.enableThreadSubscription = true // off by default +``` + +The bell tracks state on its own: it reads the current subscription state from the SDK, flips optimistically on tap, reverts if the request fails, toasts in both directions, and emits `ccThreadSubscriptionChanged` on success. You do not wire any of that up. + +If your screen already draws its own control and you would otherwise show two bells, suppress the kit's: + +```swift +messageHeaderView.hideThreadSubscriptionButton = true +``` + + + +The bell needs **both** `parentMessage` and `enableThreadSubscription`. Setting the flag alone on a conversation header renders nothing — that is deliberate, so turning the feature on globally cannot put a thread control on a non-thread screen. + + + +## Cross-Surface Sync + +Both surfaces observe the UI Kit event bus, so toggling in one place updates the other without a refetch. After a successful toggle the kit emits: + +```swift +CometChatThreadEvents.ccThreadSubscriptionChanged(parentMessageId: parentMessageId, + isSubscribed: !isSubscribed) +``` + +`CometChatMessageHeader` observes this itself, so its bell stays correct when the user toggles from the action sheet — no wiring needed. Conform to `CometChatThreadEventListener` only to keep a control of **your own** in step: + +```swift +extension ThreadedMessagesVC: CometChatThreadEventListener { + func ccThreadSubscriptionChanged(parentMessageId: Int, isSubscribed: Bool) { + guard parentMessageId == parentMessage?.id else { return } + renderThreadSubscription(isSubscribed: isSubscribed) + } +} +``` + + + +Thread listeners are keyed by id, and registering a duplicate id **evicts** the previous listener. Use a distinct id per screen — the kit randomises its own for exactly this reason. + + + +See [Events](/ui-kit/ios/events) for the full event reference. + +## Behavior + +- **Optimistic with revert** — the control flips instantly on tap, keeps one request in flight per thread, and reverts if the server rejects the change. An offline tap fails visibly and reverts; nothing is queued. +- **No event on failure** — the kit emits `ccThreadSubscriptionChanged` only on success, so every surface keeps showing the state the server still holds. +- **Toasts in both directions** — subscribing and unsubscribing each confirm with a toast, and both toggle sites use the same copy. +- **Unsubscribing is not sticky** — replying again, or being @-mentioned, re-subscribes the user. The kit says so in the toast rather than letting the user discover it. +- **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. + +## Copy and Localization + +| Key | Default copy | +| ---------------------------- | ------------------------------------------------------------------------ | +| `THREAD_SUBSCRIBE` | Subscribe to thread | +| `THREAD_UNSUBSCRIBE` | Unsubscribe from thread | +| `THREAD_SUBSCRIBED` | Following | +| `THREAD_SUBSCRIBED_TOAST` | Subscribed. You'll be notified about new replies in this thread. | +| `THREAD_UNSUBSCRIBED_TOAST` | Unsubscribed. Notifications are off until you reply or are mentioned. | +| `THREAD_SUBSCRIPTION_FAILED` | Couldn't update. Please try again. | +| `THREAD_UNAVAILABLE` | You no longer have access to this thread. | + + + +`THREAD_SUBSCRIBED` is a VoiceOver-only label for the subscribed state — it is not shown as visible text. + + + +Override any of these in your own `Localizable.strings` — see [Localize](/ui-kit/ios/localize). + +## 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/ios/thread-subscription#notification-preferences). + +## Next Steps & Further Reading + +- [Thread Subscription (SDK)](/sdk/ios/thread-subscription) — the underlying APIs, including fetching the threads a user participates in to build a thread inbox. +- [Threaded Messages Header](/ui-kit/ios/threaded-messages-header) — the full component reference. +- [Message List](/ui-kit/ios/message-list) — action-sheet options. diff --git a/ui-kit/ios/guide-threaded-messages.mdx b/ui-kit/ios/guide-threaded-messages.mdx index c81acbd75..15198b5d0 100644 --- a/ui-kit/ios/guide-threaded-messages.mdx +++ b/ui-kit/ios/guide-threaded-messages.mdx @@ -228,6 +228,9 @@ navigationItem.leftBarButtonItem = UIBarButtonItem( ## Related Guides + + Let users subscribe to or unsubscribe from a thread's replies + Customize the thread header component diff --git a/ui-kit/ios/message-header.mdx b/ui-kit/ios/message-header.mdx index fa64f531b..b806bde5c 100644 --- a/ui-kit/ios/message-header.mdx +++ b/ui-kit/ios/message-header.mdx @@ -47,8 +47,13 @@ The `CometChatMessageHeader` component displays user or group details in the too "hideVoiceCallButton": { "type": "Bool", "default": false }, "hideNewChatButton": { "type": "Bool", "default": false }, "hideChatHistoryButton": { "type": "Bool", "default": false }, + "hideThreadSubscriptionButton": { "type": "Bool", "default": false }, "disableTyping": { "type": "Bool", "default": false } }, + "threads": { + "parentMessage": { "type": "BaseMessage?", "default": "nil", "note": "Set to put the header in thread mode" }, + "enableThreadSubscription": { "type": "Bool", "default": false, "note": "Also requires parentMessage" } + }, "style": { "avatarStyle": { "type": "AvatarStyle", "default": "AvatarStyle()" }, "statusIndicatorStyle": { "type": "StatusIndicatorStyle", "default": "StatusIndicatorStyle()" }, @@ -891,6 +896,27 @@ let messageHeader = CometChatMessageHeader() messageHeader.disableTyping = true ``` +### enableThreadSubscription + +Enables the subscribe/unsubscribe bell in the header's trailing area, letting users opt in to notifications for a thread's replies. The bell **also** requires `parentMessage` — setting this flag alone on a conversation header renders nothing, so turning the feature on globally cannot put a thread control on a non-thread screen. + +The bell manages itself: it reads the current state from the SDK, flips optimistically on tap, reverts on failure, toasts in both directions, and stays in step with a toggle made from the message action sheet. + +| | | +|---|---| +| Type | `Bool` | +| Default | `false` | + +```swift lines +import CometChatUIKitSwift + +let messageHeader = CometChatMessageHeader() +messageHeader.set(parentMessage: parentMessage) +messageHeader.enableThreadSubscription = true +``` + +See the [Thread Subscription guide](/ui-kit/ios/guide-thread-subscription) for the full feature. + ### hideBackButton Hides the back button in the header. @@ -941,6 +967,15 @@ Hides the user status (online/offline/last active). | Type | `Bool` | | Default | `false` | +### hideThreadSubscriptionButton + +Hides the subscribe/unsubscribe bell while leaving the feature on — for a screen that already draws its own control and would otherwise show two. + +| | | +|---|---| +| Type | `Bool` | +| Default | `false` | + ### hideVideoCallButton Hides the video call button. @@ -999,6 +1034,22 @@ messageHeader.set(onAiNewChatClicked: { [weak self] in }) ``` +### parentMessage + +The thread's root message the subscription bell acts on. Setting it puts the header in **thread mode**; leaving it `nil` keeps the header in its normal conversation mode with no bell. + +| | | +|---|---| +| Type | `BaseMessage?` | +| Default | `nil` | + +```swift lines +import CometChatUIKitSwift + +let messageHeader = CometChatMessageHeader() +messageHeader.set(parentMessage: parentMessage) +``` + ### statusIndicatorStyle Customizes the appearance of the online/offline status indicator. diff --git a/ui-kit/ios/message-list.mdx b/ui-kit/ios/message-list.mdx index 16ec1009b..9f441fce2 100644 --- a/ui-kit/ios/message-list.mdx +++ b/ui-kit/ios/message-list.mdx @@ -56,6 +56,9 @@ The `CometChatMessageList` component displays a scrollable list of messages in a "hideMessageInfoOption": { "type": "Bool", "default": false }, "hideTranslateMessageOption": { "type": "Bool", "default": false }, "hideMessagePrivatelyOption": { "type": "Bool", "default": false }, + "hidePinMessageOption": { "type": "Bool", "default": false }, + "hideSaveMessageOption": { "type": "Bool", "default": false }, + "hideThreadSubscriptionOption": { "type": "Bool", "default": false }, "hideGroupActionMessages": { "type": "Bool", "default": false }, "hideNewMessageIndicator": { "type": "Bool", "default": false }, "hideEmptyView": { "type": "Bool", "default": false }, @@ -69,6 +72,9 @@ The `CometChatMessageList` component displays a scrollable list of messages in a "scrollToBottomOnNewMessages": { "type": "Bool", "default": true }, "startFromUnreadMessages": { "type": "Bool", "default": false }, "showMarkAsUnreadOption": { "type": "Bool", "default": false }, + "enablePinMessage": { "type": "Bool", "default": false, "note": "Also requires CometChat.isPinMessageEnabled()" }, + "enableSaveMessage": { "type": "Bool", "default": false, "note": "Also requires CometChat.isSaveMessageEnabled()" }, + "enableThreadSubscription": { "type": "Bool", "default": false, "note": "Offered on parent messages only, never on a reply" }, "messageAlignment": { "type": "MessageAlignment", "default": ".standard" } }, "viewSlots": { @@ -1471,6 +1477,58 @@ Enables AI-powered conversation summary feature. | Type | `Bool` | | Default | `false` | +### enablePinMessage + +Enables the pin and unpin options in message actions. The feature must **also** be enabled for your app — the kit checks `CometChat.isPinMessageEnabled()` — and pinning is further gated by the user's scope. + +| | | +|---|---| +| Type | `Bool` | +| Default | `false` | + +```swift lines +import CometChatUIKitSwift + +let messageList = CometChatMessageList() +messageList.enablePinMessage = true +``` + +### enableSaveMessage + +Enables the save and unsave options in message actions. The feature must **also** be enabled for your app — the kit checks `CometChat.isSaveMessageEnabled()`. + +| | | +|---|---| +| Type | `Bool` | +| Default | `false` | + +```swift lines +import CometChatUIKitSwift + +let messageList = CometChatMessageList() +messageList.enableSaveMessage = true +``` + +### enableThreadSubscription + +Enables the **Subscribe to thread** / **Unsubscribe from thread** option in message actions, letting users opt in to notifications for a thread's replies. The option is offered on **parent messages only** — never on a reply inside a thread, because a subscription is always rooted at the thread's parent. It is not gated on reply count: subscribing to a message that has no replies yet is supported. + +Unlike some other CometChat platforms, iOS offers this in one-on-one conversations as well as groups. + +| | | +|---|---| +| Type | `Bool` | +| Default | `false` | + +```swift lines +import CometChatUIKitSwift + +let messageList = CometChatMessageList() +messageList.enableThreadSubscription = true +``` + +See the [Thread Subscription guide](/ui-kit/ios/guide-thread-subscription) for the full feature, including keeping your own subscription control in sync. + ### enableSmartReplies Enables AI-powered smart reply suggestions. @@ -1670,6 +1728,15 @@ Hides the new message indicator. | Type | `Bool` | | Default | `false` | +### hidePinMessageOption + +Hides the pin option in message actions. Independent of `enablePinMessage` — use this to suppress the option on one screen while the feature stays on elsewhere. + +| | | +|---|---| +| Type | `Bool` | +| Default | `false` | + ### hideReactionOption Hides the reaction option on messages. @@ -1713,6 +1780,38 @@ let messageList = CometChatMessageList() messageList.hideReplyMessageOption = true ``` +### hideSaveMessageOption + +Hides the save option in message actions. Independent of `enableSaveMessage` — use this to suppress the option on one screen while the feature stays on elsewhere. + +| | | +|---|---| +| Type | `Bool` | +| Default | `false` | + +```swift lines +import CometChatUIKitSwift + +let messageList = CometChatMessageList() +messageList.hideSaveMessageOption = true +``` + +### hideThreadSubscriptionOption + +Hides the subscribe/unsubscribe option in message actions. Independent of `enableThreadSubscription` — use this to suppress the option on one screen while the feature stays on elsewhere. Ignored while `enableThreadSubscription` is `false`, since nothing renders in that case anyway. + +| | | +|---|---| +| Type | `Bool` | +| Default | `false` | + +```swift lines +import CometChatUIKitSwift + +let messageList = CometChatMessageList() +messageList.hideThreadSubscriptionOption = true +``` + ### hideShareMessageOption Hides the share message option in message actions. diff --git a/ui-kit/ios/message-template.mdx b/ui-kit/ios/message-template.mdx index 6fff04600..7e67d3bee 100644 --- a/ui-kit/ios/message-template.mdx +++ b/ui-kit/ios/message-template.mdx @@ -834,6 +834,10 @@ The `template.options` method in the MessageTemplate allows you to customize the However, if you wish to override or modify these options, you can use the `template.options` method and pass a list of `getMessageOptions`. This list of options will replace the default set. + +When pin and save are enabled, four more option ids appear in this list: `pinMessage`, `unpinMessage`, `saveMessage` and `unsaveMessage` (available as constants on `MessageOptionConstants`). By default they render behind a "More…" row rather than inline — control that with `MessageOptionConstants.overflowOptionIds`. See the [Pin & Save Messages guide](/ui-kit/ios/guide-pin-save-message). + + diff --git a/ui-kit/ios/pinned-messages.mdx b/ui-kit/ios/pinned-messages.mdx new file mode 100644 index 000000000..103ed03df --- /dev/null +++ b/ui-kit/ios/pinned-messages.mdx @@ -0,0 +1,521 @@ +--- +title: "Pinned Messages" +description: "Display and manage CometChat iOS UI Kit pinned messages for a conversation with per-row unpin, jump-to-message, custom view slots, and styling." +--- + +`CometChatPinnedMessages` is a full-screen list of the messages pinned in a single conversation, newest pin first. Pins are conversation-wide: everyone in the chat sees the same list, and moderators can unpin from any row. + + +```json +{ + "component": "CometChatPinnedMessages", + "package": "CometChatUIKitSwift", + "import": "import CometChatUIKitSwift\nimport CometChatSDK", + "description": "Full-screen list of messages pinned in one conversation, ordered newest pin first", + "inherits": "CometChatListBase", + "primaryOutput": { + "callback": "onMessageClicked", + "type": "(BaseMessage) -> Void" + }, + "props": { + "data": { + "user": { "type": "User?", "required": false, "note": "Pass user OR group, not both" }, + "group": { "type": "Group?", "required": false }, + "requestBuilder": { "type": "MessagesRequest.MessageRequestBuilder", "note": "Must retain set(pinned: true)" } + }, + "callbacks": { + "onMessageClicked": "(BaseMessage) -> Void", + "onError": "(CometChatException) -> Void", + "onLoad": "([BaseMessage]) -> Void", + "onEmpty": "() -> Void", + "onBack": "() -> Void" + }, + "visibility": { + "hideUnpinOption": { "type": "Bool", "default": false } + }, + "styling": { + "style": { "type": "PinnedMessagesStyle" }, + "messageBubbleStyle": { "type": "(incoming: MessageBubbleStyle, outgoing: MessageBubbleStyle)" }, + "dateSeparatorStyle": { "type": "DateStyle" }, + "avatarStyle": { "type": "AvatarStyle" } + }, + "viewSlots": { + "titleView": "(BaseMessage?) -> UIView", + "subtitle": "(BaseMessage?) -> UIView", + "trailingView": "(BaseMessage?) -> UIView", + "listItemView": "(BaseMessage?) -> UIView" + } + }, + "events": ["onMessagePinned", "onMessageUnpinned", "ccMessagePinned"], + "sdkListeners": ["CometChatConnectionDelegate"], + "compositionExample": { + "description": "Opened from the message header overflow menu, jumps the message list on row tap", + "components": ["CometChatMessageHeader", "CometChatPinnedMessages", "CometChatMessageList"], + "flow": "User taps ⋮ → Pinned messages → taps a row → returns to chat scrolled to that message" + } +} +``` + + +| Field | Value | +|-------|-------| +| Component | `CometChatPinnedMessages` | +| Package | `CometChatUIKitSwift` | +| Inherits | `CometChatListBase` | + +--- + +## Where It Fits + +Pinned messages are scoped to one conversation, so this screen is opened from that conversation — typically the overflow menu in `CometChatMessageHeader`. Tapping a row takes the user back to the chat and scrolls to the message. + +Contrast with [Saved Messages](/ui-kit/ios/saved-messages), which is a user-level screen spanning every conversation and is reached from your app's chrome instead. + + +Pin and save are **off by default**. Set `enablePinMessage` on your `CometChatMessageList` and enable the feature for your app, or no pin options appear and this screen stays empty. See the [Pin and Save Messages guide](/ui-kit/ios/guide-pin-save-message). + + +This screen is read-only by design: opening it never marks anything as read, sends receipts, or changes the unread count. + +--- + +## Minimal Render + +`CometChatPinnedMessages` is a view controller, so push it onto your navigation stack. + +```swift lines +import UIKit +import CometChatUIKitSwift +import CometChatSDK + +// For a group conversation +let pinnedVC = CometChatPinnedMessages(group: group) +navigationController?.pushViewController(pinnedVC, animated: true) + +// For a one-to-one conversation +let pinnedVC = CometChatPinnedMessages(user: user) +navigationController?.pushViewController(pinnedVC, animated: true) +``` + +Pass either `user` or `group` — they are mutually exclusive and scope the list to that conversation. + +--- + +## Filtering + +The list is fetched with a `MessagesRequest.MessageRequestBuilder`. The default comes from `PinnedMessagesBuilder`: + +```swift lines +// The default builder used when you set nothing +MessagesRequest.MessageRequestBuilder() + .set(limit: 100) + .set(pinned: true) +``` + +The server caps pins at 100 per conversation, so a limit of 100 fetches the entire list in one page. + +To narrow the list further, supply your own builder: + +```swift lines +// MARK: - Only pinned text messages +let requestBuilder = MessagesRequest.MessageRequestBuilder() + .set(limit: 100) + .set(pinned: true) + .set(guid: group.guid) + .set(types: ["text"]) + +let pinnedVC = CometChatPinnedMessages(group: group) +pinnedVC.set(requestBuilder: requestBuilder) +``` + + +A custom request builder **must keep `set(pinned: true)`**. Without it the request returns every message in the conversation and the screen lists them all as though they were pinned. + + +--- + +## Actions and Events + +### Callback Props + +#### onMessageClicked + +Fires when a row is tapped. Use it to return to the conversation and jump to the message. + +```swift lines +pinnedVC.set(onMessageClicked: { [weak self] message in + self?.navigationController?.popViewController(animated: true) + self?.messageListView.goToMessage(withId: message.id) +}) +``` + +#### onError + +Fires when the fetch fails, and again when an unpin fails. + +```swift lines +pinnedVC.set(onError: { error in + print("Pinned messages error: \(error.errorCode)") +}) +``` + +#### onLoad + +Fires with the fetched messages each time the list reloads. + +```swift lines +pinnedVC.set(onLoad: { messages in + print("Loaded \(messages.count) pinned messages") +}) +``` + +#### onEmpty + +Fires when the fetch completes with no pinned messages. + +```swift lines +pinnedVC.set(onEmpty: { + print("No pinned messages in this conversation") +}) +``` + +#### onBack + +Inherited from `CometChatListBase`. The component ships a default that pops the navigation stack; set your own to replace it. + +```swift lines +pinnedVC.set(onBack: { [weak self] in + self?.navigationController?.popViewController(animated: true) +}) +``` + +### Actions Reference + +| Action | Trigger | Default behavior | +|--------|---------|------------------| +| Row tap | User taps a pinned message | Calls `onMessageClicked`; no default navigation | +| Unpin | User swipes a row from the trailing edge | Unpins the message and removes the row | +| Back | User taps the back chevron | Pops the navigation stack | +| Pull to refresh | User pulls the list down | Refetches the pinned list | + +### Global UI Events + +Pin and unpin actions performed elsewhere in the kit emit on `CometChatMessageEvents`. See [Events](/ui-kit/ios/events). + +| Event | Meaning | +|-------|---------| +| `ccMessagePinned` | The local user pinned or unpinned a message. Read `message.pinnedAt` to tell which — non-zero means pinned. | + +--- + +## Custom View Slots + +Each row is a `CometChatMessageBubble` built from the same message templates the message +list uses, so pinned photos, videos and files render as real bubbles. Rows are left-aligned +regardless of sender — including your own, which keep their outgoing bubble color but sit on +the left with an avatar and name. Each slot below receives the row's `BaseMessage` and +returns a view that replaces one part of that bubble. + +### set(titleView:) + +Replaces the bubble's header, which carries the sender name by default. + +```swift lines +pinnedVC.set(titleView: { message in + let label = UILabel() + label.text = message?.sender?.name ?? "" + label.font = .systemFont(ofSize: 16, weight: .semibold) + return label +}) +``` + +### set(subtitle:) + +Replaces the bubble's content — the rendered message body. + +```swift lines +pinnedVC.set(subtitle: { message in + let label = UILabel() + label.text = (message as? TextMessage)?.text ?? "" + label.textColor = .secondaryLabel + return label +}) +``` + +### set(trailingView:) + +Replaces the bubble's status-info slot, which holds the timestamp and read receipt. + +### set(listItemView:) + +Replaces the entire bubble. Use this when the slots above are not enough. + +```swift lines +pinnedVC.set(listItemView: { message in + let container = UIView() + // Build your own row layout + return container +}) +``` + +### Message templates + +To change how one message *type* renders, override its template rather than a slot. This +keeps every other type on its default bubble. + +```swift lines +pinnedVC.add(template: myCustomTextTemplate) // override a single category/type +pinnedVC.set(templates: allMyTemplates) // replace every default +``` + +--- + +## Styling + +### Style Hierarchy + +`PinnedMessagesStyle` conforms to `ListBaseStyle`, so it carries the standard screen-level properties plus the pin-specific ones. Bubble appearance is separate — set it through `messageBubbleStyle`. + +### Global Level Styling + +Applies to every instance created afterwards. + +```swift lines +// MARK: - Apply global styling +CometChatPinnedMessages.style.backgroundColor = UIColor(hex: "#F76808") +CometChatPinnedMessages.style.unpinActionBackgroundColor = UIColor(hex: "#D92D20") +``` + +### Instance Level Styling + +```swift lines +// MARK: - Apply instance-level styling +var customStyle = PinnedMessagesStyle() +customStyle.backgroundColor = UIColor(hex: "#F76808") +customStyle.bubbleHeaderTextColor = CometChatTheme.textColorPrimary +customStyle.unpinIconTint = UIColor(hex: "#FFFFFF") + +let pinnedVC = CometChatPinnedMessages(group: group) +pinnedVC.set(style: customStyle) +``` + +### Key Style Properties + +| Property | Description | Default | +|----------|-------------|---------| +| `unpinIconTint` | Tint for the per-row unpin control. | `CometChatTheme.iconColorSecondary` | +| `unpinActionBackgroundColor` | Background of the unpin swipe action. | `CometChatTheme.errorColor` | +| `backgroundColor` | Screen background. | `CometChatTheme.backgroundColor01` | +| `titleColor` | Navigation title color. | `CometChatTheme.textColorPrimary` | +| `bubbleHeaderTextColor` | Sender-name color inside the bubble. | `CometChatTheme.primaryColor` | +| `bubbleHeaderFont` | Sender-name font inside the bubble. | `CometChatTypography.Caption1.medium` | +| `previewTextColor` | Preview color used by the pinned banner. | `CometChatTheme.textColorSecondary` | +| `previewFont` | Preview font used by the pinned banner. | `CometChatTypography.Body.regular` | +| `emptyTitleTextColor` | Empty-state title color. | `CometChatTheme.textColorPrimary` | +| `errorTitleTextColor` | Error-state title color. | `CometChatTheme.textColorPrimary` | + +Bubbles are styled through `messageBubbleStyle`, the date dividers through `dateSeparatorStyle`, and avatars through `avatarStyle`: + +```swift lines +pinnedVC.messageBubbleStyle.incoming.backgroundColor = UIColor(hex: "#E9EAEB") +pinnedVC.dateSeparatorStyle.textColor = CometChatTheme.textColorSecondary +pinnedVC.avatarStyle.cornerRadius = CometChatCornerStyle(cornerRadius: 24) +``` + +Rows group under a date divider per day, ordered newest pin first. Hide the dividers with +`set(hideDateSeparator: true)`. + +### Customization Matrix + +| What to change | Where | Property/API | +|----------------|-------|--------------| +| Unpin swipe color | Style | `style.unpinActionBackgroundColor` | +| Hide unpin entirely | Prop | `set(hideUnpinOption: true)` | +| Bubble appearance | Style | `messageBubbleStyle` | +| One message type's bubble | Template | `add(template:)` | +| Whole row layout | View slot | `set(listItemView:)` | +| Hide date dividers | Prop | `set(hideDateSeparator: true)` | +| Bubble alignment | Prop | `set(messageAlignment:)` | +| Which messages appear | Filter | `set(requestBuilder:)` | +| Timestamp format | Formatter | `dateTimeFormatter` | +| Row tap behavior | Callback | `set(onMessageClicked:)` | + +--- + +## Props + +All props are optional. Sorted alphabetically. + +### avatarStyle + +Styling for the sender avatar beside incoming bubbles. + +| | | +|---|---| +| Type | `AvatarStyle` | +| Default | `CometChatAvatar.style` | + +### dateSeparatorStyle + +Styling for the per-day date divider above each group of rows. + +| | | +|---|---| +| Type | `DateStyle` | +| Default | `CometChatDate.style` | + +### messageBubbleStyle + +Appearance of the incoming and outgoing bubbles. + +| | | +|---|---| +| Type | `(incoming: MessageBubbleStyle, outgoing: MessageBubbleStyle)` | +| Default | `CometChatMessageBubble.style` | + +### messageAlignment + +Every pinned message is left-aligned by default, the logged-in user's included, so each row +is attributed by its avatar and sender name rather than by position. Set `.standard` to +mirror the message list and align your own messages right. + +| | | +|---|---| +| Type | `MessageListAlignment` | +| Default | `.leftAligned` | + +### hideDateSeparator + +Hides the per-day date dividers. + +| | | +|---|---| +| Type | `Bool` | +| Default | `false` | + +### dateTimeFormatter + +Custom timestamp formatting. + +| | | +|---|---| +| Type | `CometChatDateTimeFormatter` | +| Default | `CometChatUIKit.dateTimeFormatter` | + +### group + +The group whose pinned messages to show. Mutually exclusive with `user`; pass it to the initializer. + +| | | +|---|---| +| Type | `Group?` | +| Default | `nil` | + +### hideUnpinOption + +Hides the per-row unpin swipe action. Set this for users who cannot pin in this conversation. + +| | | +|---|---| +| Type | `Bool` | +| Default | `false` | + +```swift lines +pinnedVC.set(hideUnpinOption: true) +``` + +### style + +The component's style object. + +| | | +|---|---| +| Type | `PinnedMessagesStyle` | +| Default | `PinnedMessagesStyle()` | + +### user + +The user whose pinned messages to show. Mutually exclusive with `group`; pass it to the initializer. + +| | | +|---|---| +| Type | `User?` | +| Default | `nil` | + +--- + +## Methods + +### set(requestBuilder:) + +Replaces the request used to fetch the list. Must retain `set(pinned: true)`. + +### set(textFormatters:) + +Applies custom text formatters to the message previews, matching the formatters used in your message list. + +```swift lines +pinnedVC.set(textFormatters: [myCustomTextFormatter]) +``` + + +`PinnedMessagesViewModel` is public in name only — every member except `setRequestBuilder(requestBuilder:)` is internal. Customize through the props and view slots above rather than the view model. + + +--- + +## Common Patterns + +### Open from the message header and jump to the message + +The complete round trip: open the panel from the conversation, then return and scroll to the tapped message. + +```swift lines +private func openPinnedMessages() { + let pinnedVC = CometChatPinnedMessages(user: user, group: group) + + // Tapping a row returns to this conversation and jumps to the message. + pinnedVC.set(onMessageClicked: { [weak self] message in + guard let self = self else { return } + self.navigationController?.popViewController(animated: true) + self.messageListView.goToMessage(withId: message.id) + }) + + navigationController?.pushViewController(pinnedVC, animated: true) +} +``` + +### Hide unpin for users without permission + +Pinning is restricted to group owners, admins and moderators. Hide the unpin action for everyone else so the swipe does not fail against the server. + +```swift lines +let pinnedVC = CometChatPinnedMessages(group: group) +pinnedVC.set(hideUnpinOption: !GroupMembersUtils.allowPinMessage(group: group)) +``` + +### Custom empty state + +```swift lines +let pinnedVC = CometChatPinnedMessages(group: group) +pinnedVC.emptyStateTitleText = "Nothing pinned yet" +pinnedVC.emptyStateSubTitleText = "Pin important messages to find them here." +``` + +--- + +## Related Components + +- [Saved Messages](/ui-kit/ios/saved-messages) - The user-level saved messages screen +- [Message List](/ui-kit/ios/message-list) - Where messages are pinned and unpinned +- [Message Header](/ui-kit/ios/message-header) - Hosts the menu that opens this screen +- [Events](/ui-kit/ios/events) - Pin and save event callbacks + + + + End-to-end setup for pinning and saving + + + The underlying SDK methods and message fields + + diff --git a/ui-kit/ios/saved-messages.mdx b/ui-kit/ios/saved-messages.mdx new file mode 100644 index 000000000..4c59ccb1c --- /dev/null +++ b/ui-kit/ios/saved-messages.mdx @@ -0,0 +1,455 @@ +--- +title: "Saved Messages" +description: "Display CometChat iOS UI Kit saved messages for the logged-in user across every conversation, with per-row unsave, source labels, and tap to open the chat." +--- + +`CometChatSavedMessages` is a full-screen list of the messages the logged-in user has saved, newest save first. Saves are **private to the user** and **span every conversation**, so each row is labelled with the chat it came from. + + +```json +{ + "component": "CometChatSavedMessages", + "package": "CometChatUIKitSwift", + "import": "import CometChatUIKitSwift\nimport CometChatSDK", + "description": "Full-screen, user-level list of saved messages spanning all conversations, newest save first", + "inherits": "CometChatListBase", + "primaryOutput": { + "callback": "onMessageClicked", + "type": "(BaseMessage) -> Void" + }, + "props": { + "data": { + "requestBuilder": { "type": "MessagesRequest.MessageRequestBuilder", "note": "Must retain set(saved: true); takes no uid/guid" } + }, + "callbacks": { + "onMessageClicked": "(BaseMessage) -> Void", + "onError": "(CometChatException) -> Void", + "onLoad": "([BaseMessage]) -> Void", + "onEmpty": "() -> Void", + "onBack": "() -> Void" + }, + "visibility": { + "hideUnsaveOption": { "type": "Bool", "default": false } + }, + "styling": { + "style": { "type": "SavedMessagesStyle" }, + "dateStyle": { "type": "DateStyle" }, + "avatarStyle": { "type": "AvatarStyle" } + }, + "viewSlots": { + "titleView": "(BaseMessage?) -> UIView", + "subtitle": "(BaseMessage?) -> UIView", + "leadingView": "(BaseMessage?) -> UIView", + "trailingView": "(BaseMessage?) -> UIView", + "listItemView": "(BaseMessage?) -> UIView" + } + }, + "events": ["onMessageSaved", "onMessageUnsaved", "ccMessageSaved"], + "sdkListeners": ["CometChatConnectionDelegate"], + "compositionExample": { + "description": "Opened from app chrome, not a conversation header, because saves are user-level", + "components": ["CometChatSavedMessages", "CometChatMessageList"], + "flow": "User opens Saved messages from the chats screen menu → taps a row → the source conversation opens" + } +} +``` + + +| Field | Value | +|-------|-------| +| Component | `CometChatSavedMessages` | +| Package | `CometChatUIKitSwift` | +| Inherits | `CometChatListBase` | + +--- + +## Where It Fits + + +This is a **user-level** screen, not a per-conversation one. It takes no `user` or `group` — it always shows the logged-in user's saves across every chat. Open it from your app's chrome (a tab, the chats-screen menu, a profile or settings entry), **not** from a conversation header. + + +Contrast with [Pinned Messages](/ui-kit/ios/pinned-messages), which is scoped to one conversation and visible to everyone in it. Saves are private: only the saving user sees them, synced across that user's own devices. + +Pin and save are **off by default** — see the [Pin and Save Messages guide](/ui-kit/ios/guide-pin-save-message) to turn them on. + +This screen is read-only by design: opening it never marks anything as read, sends receipts, or changes the unread count. + +--- + +## Minimal Render + +`CometChatSavedMessages` is a view controller with a no-argument initializer. + +```swift lines +import UIKit +import CometChatUIKitSwift +import CometChatSDK + +let savedVC = CometChatSavedMessages() +navigationController?.pushViewController(savedVC, animated: true) +``` + + +If you push this screen from a tab that hides the navigation bar, set `hideNavigationBar = false` or the title and back button never appear. + +Do **not** also set `hideBackButton = false`. The component supplies its own back chevron as a left bar button item, so clearing that flag renders two back buttons side by side. + + +```swift lines +let savedVC = CometChatSavedMessages() +savedVC.hidesBottomBarWhenPushed = true +savedVC.hideNavigationBar = false + +navigationController?.setNavigationBarHidden(false, animated: true) +navigationController?.pushViewController(savedVC, animated: true) +``` + +--- + +## Filtering + +The list is fetched with a `MessagesRequest.MessageRequestBuilder`. The default comes from `SavedMessagesBuilder`: + +```swift lines +// The default builder used when you set nothing +MessagesRequest.MessageRequestBuilder() + .set(limit: 100) + .set(saved: true) +``` + +Unlike the pinned builder there is no `uid` or `guid` to set — saved messages are a per-user collection spanning every conversation. The server caps saves at 100, so a limit of 100 fetches the entire list in one page. + +```swift lines +// MARK: - Only saved text messages +let requestBuilder = MessagesRequest.MessageRequestBuilder() + .set(limit: 100) + .set(saved: true) + .set(types: ["text"]) + +let savedVC = CometChatSavedMessages() +savedVC.set(requestBuilder: requestBuilder) +``` + + +A custom request builder **must keep `set(saved: true)`**. Without it the request returns every message the user can see and the screen lists them all as though they were saved. + + +--- + +## Actions and Events + +### Callback Props + +#### onMessageClicked + +Fires when a row is tapped. Because a row can belong to any conversation, use it to open that message's own chat — see [Common Patterns](#common-patterns). + +```swift lines +savedVC.set(onMessageClicked: { [weak self] message in + self?.openConversation(for: message) +}) +``` + +#### onError + +Fires when the fetch fails, and again when an unsave fails. + +```swift lines +savedVC.set(onError: { error in + print("Saved messages error: \(error.errorCode)") +}) +``` + +#### onLoad + +Fires with the fetched messages each time the list reloads. + +```swift lines +savedVC.set(onLoad: { messages in + print("Loaded \(messages.count) saved messages") +}) +``` + +#### onEmpty + +Fires when the fetch completes with nothing saved. + +#### onBack + +Inherited from `CometChatListBase`. The component ships a default that pops the navigation stack; set your own to replace it — useful when you also need to re-hide the navigation bar on the way out. + +```swift lines +savedVC.set(onBack: { [weak self] in + self?.navigationController?.setNavigationBarHidden(true, animated: true) + self?.navigationController?.popViewController(animated: true) +}) +``` + +### Actions Reference + +| Action | Trigger | Default behavior | +|--------|---------|------------------| +| Row tap | User taps a saved message | Calls `onMessageClicked`; no default navigation | +| Unsave | User swipes a row from the trailing edge | Unsaves the message, removes the row, shows a toast | +| Back | User taps the back chevron | Pops the navigation stack | +| Pull to refresh | User pulls the list down | Refetches the saved list | + +### Global UI Events + +| Event | Meaning | +|-------|---------| +| `ccMessageSaved` | The local user saved or unsaved a message. Read `message.savedAt` to tell which — non-zero means saved. | + +See [Events](/ui-kit/ios/events) for the full listener surface. + +--- + +## Custom View Slots + +Each slot receives the row's `BaseMessage` and returns a view that replaces the default. + +### set(titleView:) + +Replaces the row title, which by default is the **source conversation** name rather than the sender. + +```swift lines +savedVC.set(titleView: { message in + let label = UILabel() + label.text = message?.sender?.name ?? "" + return label +}) +``` + +### set(subtitle:) + +Replaces the message-preview line. + +### set(leadingView:) + +Replaces the leading slot, which holds the source conversation's avatar by default. + +### set(trailingView:) + +Replaces the trailing slot, which holds the timestamp by default. + +### set(listItemView:) + +Replaces the entire row. + +--- + +## Styling + +### Style Hierarchy + +`SavedMessagesStyle` conforms to `ListBaseStyle` and `ListItemStyle`, so it carries the standard list properties plus two save-specific ones. + +### Global Level Styling + +```swift lines +// MARK: - Apply global styling +CometChatSavedMessages.style.backgroundColor = UIColor(hex: "#F76808") +CometChatSavedMessages.style.unsaveActionBackgroundColor = UIColor(hex: "#D92D20") +``` + +### Instance Level Styling + +```swift lines +// MARK: - Apply instance-level styling +var customStyle = SavedMessagesStyle() +customStyle.backgroundColor = UIColor(hex: "#F76808") +customStyle.messageTypeImageTint = CometChatTheme.iconColorHighlight + +let savedVC = CometChatSavedMessages() +savedVC.set(style: customStyle) +``` + +### Key Style Properties + +| Property | Description | Default | +|----------|-------------|---------| +| `messageTypeImageTint` | Tint of the message-type glyph leading the preview (photo, video, document). | `CometChatTheme.iconColorSecondary` | +| `unsaveActionBackgroundColor` | Background of the unsave swipe action. | `CometChatTheme.errorColor` | +| `backgroundColor` | Screen background. | `CometChatTheme.backgroundColor01` | +| `titleColor` | Navigation title color. | `CometChatTheme.textColorPrimary` | +| `listItemTitleTextColor` | Row title color. | `CometChatTheme.textColorPrimary` | +| `listItemTitleFont` | Row title font. | `CometChatTypography.Heading4.medium` | +| `listItemSubTitleTextColor` | Row preview color. | `CometChatTheme.textColorSecondary` | +| `listItemSubTitleFont` | Row preview font. | `CometChatTypography.Body.regular` | +| `emptyTitleTextColor` | Empty-state title color. | `CometChatTheme.textColorPrimary` | +| `errorTitleTextColor` | Error-state title color. | `CometChatTheme.textColorPrimary` | + +### Customization Matrix + +| What to change | Where | Property/API | +|----------------|-------|--------------| +| Unsave swipe color | Style | `style.unsaveActionBackgroundColor` | +| Hide unsave entirely | Prop | `set(hideUnsaveOption: true)` | +| Media glyph tint | Style | `style.messageTypeImageTint` | +| Row layout | View slot | `set(listItemView:)` | +| Which messages appear | Filter | `set(requestBuilder:)` | +| Row tap behavior | Callback | `set(onMessageClicked:)` | + +--- + +## Props + +All props are optional. Sorted alphabetically. + +### avatarStyle + +Styling for the row avatar, which shows the source conversation. + +| | | +|---|---| +| Type | `AvatarStyle` | +| Default | `CometChatAvatar.style` | + +### dateStyle + +Styling for the row timestamp. + +| | | +|---|---| +| Type | `DateStyle` | +| Default | `CometChatDate.style`, flattened to bare text | + +### dateTimeFormatter + +Custom timestamp formatting. + +| | | +|---|---| +| Type | `CometChatDateTimeFormatter` | +| Default | `CometChatUIKit.dateTimeFormatter` | + +### hideUnsaveOption + +Hides the per-row unsave swipe action. + +| | | +|---|---| +| Type | `Bool` | +| Default | `false` | + +```swift lines +savedVC.set(hideUnsaveOption: true) +``` + +### style + +The component's style object. + +| | | +|---|---| +| Type | `SavedMessagesStyle` | +| Default | `SavedMessagesStyle()` | + +--- + +## Methods + +### set(requestBuilder:) + +Replaces the request used to fetch the list. Must retain `set(saved: true)`. + +### set(textFormatters:) + +Applies custom text formatters to the message previews. + +```swift lines +savedVC.set(textFormatters: [myCustomTextFormatter]) +``` + + +`SavedMessagesViewModel` is public in name only — every member except `setRequestBuilder(requestBuilder:)` is internal. Customize through the props and view slots above rather than the view model. + + +--- + +## Common Patterns + +### Open from the chats screen menu + +Saves are user-level, so the entry point belongs in app chrome rather than a conversation. + +```swift lines +private func openSavedMessages() { + let savedVC = CometChatSavedMessages() + savedVC.hidesBottomBarWhenPushed = true + + // The component supplies its own back chevron, so leave hideBackButton alone. + savedVC.hideNavigationBar = false + + savedVC.set(onBack: { [weak self] in + self?.navigationController?.setNavigationBarHidden(true, animated: true) + self?.navigationController?.popViewController(animated: true) + }) + + savedVC.set(onMessageClicked: { [weak self] message in + self?.openConversation(for: message) + }) + + // The chats screen hides the bar; unhide it before pushing. + navigationController?.setNavigationBarHidden(false, animated: true) + navigationController?.pushViewController(savedVC, animated: true) +} +``` + +### Open the conversation a row came from + +A saved row can belong to any chat, so resolve the conversation from the message before navigating. For a group, look it up by `receiverUid`. For one-to-one, a **received** message is addressed to the logged-in user, so the conversation is with its sender. + +```swift lines +private func openConversation(for message: BaseMessage) { + let openMessages: (CometChatSDK.User?, Group?) -> Void = { [weak self] user, group in + guard let self = self, user != nil || group != nil else { return } + let messages = MessagesVC() + messages.user = user + messages.group = group + self.navigationController?.pushViewController(messages, animated: true) + } + + if message.receiverType == .group { + CometChat.getGroup(GUID: message.receiverUid) { group in + DispatchQueue.main.async { openMessages(nil, group) } + } onError: { _ in } + } else { + // A received 1:1 message is addressed to the logged-in user, so the + // conversation is with its sender; one they sent is addressed to the other party. + let isReceived = message.receiverUid == CometChat.getLoggedInUser()?.uid + let uid = isReceived ? (message.sender?.uid ?? message.receiverUid) : message.receiverUid + + CometChat.getUser(UID: uid) { user in + DispatchQueue.main.async { openMessages(user, nil) } + } onError: { _ in } + } +} +``` + +### Custom empty state + +```swift lines +let savedVC = CometChatSavedMessages() +savedVC.emptyStateTitleText = "Nothing saved yet" +savedVC.emptyStateSubTitleText = "Save messages to read them later." +``` + +--- + +## Related Components + +- [Pinned Messages](/ui-kit/ios/pinned-messages) - The per-conversation pinned messages screen +- [Message List](/ui-kit/ios/message-list) - Where messages are saved and unsaved +- [Conversations](/ui-kit/ios/conversations) - The chats screen that hosts the entry point +- [Events](/ui-kit/ios/events) - Pin and save event callbacks + + + + End-to-end setup for pinning and saving + + + The underlying SDK methods and message fields + + From 1cae4991cd074b387bc27f3eba8a101489f19007 Mon Sep 17 00:00:00 2001 From: shreeyajoshi-cometchat Date: Mon, 7 Sep 2026 21:22:46 +0530 Subject: [PATCH 2/5] docs(ui-kit/ios,sdk/ios): catch up with the pin/save + thread PRs The pages were written against the state of the two feature branches on 10 Aug. Both moved since, and the SDK removed an API the docs still taught. Removed APIs, replaced with what shipped: - CometChat.threadSubscriptionState(forParentMessageId:) and its SUBSCRIBED/NOT_SUBSCRIBED/UNKNOWN tri-state are gone. The SDK stopped caching subscription state it could not fully observe; the server flag now rides on each message as BaseMessage.threadSubscribed. Documents the fetched-vs-not-reported distinction and the local-only setter. - CometChatThreadDelegate, ThreadSubscriptionEvent, ThreadReplyEvent and their two callbacks no longer exist. Replaced the real-time section with how to actually stay in sync, and a warning against rebuilding the local cache the SDK deliberately dropped. - MessageThread.subscriptionState -> isSubscribed. - core-features.mdx used the removed synchronous read; it now shows the kit's ccThreadSubscriptionChanged listener instead. Newly documented: - CometChatException.errorParams, with a worked example on the pin limit and a note that it is not ObjC-representable. - The compound (updatedAt, id) thread-list cursor. - QuotedRepliesOptions as an enum distinct from RepliesOptions, since value 4 means something different on each. - enableThreadSubscription on CometChatPinnedMessages. UI Kit surface changes: - the bell moved to CometChatThreadedMessageHeader as a second surface; enable it on exactly one of the two headers - enableThreadSubscription/hideThreadSubscriptionButton take their set() builder form on the headers - subscription is offered in 1-1 conversations again, after the group-only detour - the bell needs a parent message that has been sent - gallery captions inherit the text bubble's colour Also drops a duplicated sentence in message-list.mdx. Verified every symbol against feat/pin-save-threads-4.1.9 (SDK) and feat/pin-save-thread (UI Kit) rather than the changelogs. Co-Authored-By: Claude Opus 5 (1M context) --- sdk/ios/pin-save-message.mdx | 25 ++++- sdk/ios/thread-subscription.mdx | 119 ++++++++++++++--------- ui-kit/ios/core-features.mdx | 9 +- ui-kit/ios/guide-thread-subscription.mdx | 14 +-- ui-kit/ios/message-bubble-styling.mdx | 8 +- ui-kit/ios/message-header.mdx | 4 +- ui-kit/ios/message-list.mdx | 4 +- ui-kit/ios/pinned-messages.mdx | 13 +++ ui-kit/ios/threaded-messages-header.mdx | 25 +++++ 9 files changed, 157 insertions(+), 64 deletions(-) diff --git a/sdk/ios/pin-save-message.mdx b/sdk/ios/pin-save-message.mdx index ef3e44d76..9c3d16443 100644 --- a/sdk/ios/pin-save-message.mdx +++ b/sdk/ios/pin-save-message.mdx @@ -284,7 +284,30 @@ if CometChat.isSaveMessageEnabled() { | `ERR_MESSAGE_ACTION_NOT_ALLOWED` | The action is not allowed on this message. | | `ERR_FEATURE_NOT_ACCESSIBLE` | The feature is not enabled for this app. | -For the two limit errors, read the actual cap from the exception's `errorParams` rather than hard-coding a number — the limit is server-owned and may change. +`CometChatException` carries an optional `errorParams` dictionary holding structured, machine-readable details the server sent alongside the error — so you can react to an error programmatically instead of parsing `errorDescription`. + + + +```swift +CometChat.pinMessage(messageId: messageId) { _ in + // pinned +} onError: { error in + if error.errorCode == "ERR_PINNED_MESSAGES_LIMIT_EXCEEDED", + let limit = error.errorParams?["limit"] as? Int { + print("This conversation allows at most \(limit) pinned messages") + } +} +``` + + + +For the two limit errors, read the actual cap from `errorParams` rather than hard-coding a number — the limit is server-owned and may change. + + + +`errorParams` is `[String: Any]?` and is therefore **not** exposed to Objective-C. It is `nil` when the server sent no structured details, so always unwrap it. + + --- diff --git a/sdk/ios/thread-subscription.mdx b/sdk/ios/thread-subscription.mdx index 3083911b2..e4eeb1d34 100644 --- a/sdk/ios/thread-subscription.mdx +++ b/sdk/ios/thread-subscription.mdx @@ -52,36 +52,51 @@ Unsubscribing is **not sticky**. If the user replies in the thread again, or is -## Get the Subscription State +## Read the Subscription State -`threadSubscriptionState(forParentMessageId:)` 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. +Every fetched message carries the logged-in user's subscription state for its own thread on `BaseMessage.threadSubscribed`. It arrives with the message fetch, so rendering a subscribe control needs **no extra network call** and no separate state cache. ```swift -switch CometChat.threadSubscriptionState(forParentMessageId: parentMessageId) { -case .SUBSCRIBED: break // render "Unsubscribe from thread" -case .NOT_SUBSCRIBED: break // render "Subscribe to thread" -case .UNKNOWN: break // render "Subscribe to thread" -@unknown default: break +if message.threadSubscribed { + // render "Unsubscribe from thread" +} else { + // render "Subscribe to thread" } ``` -The state is a deliberate tri-state, not a boolean: +Because it is a plain `Bool`, read it only off a message you actually fetched: -| 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). | +| Situation | `threadSubscribed` | How to treat it | +| --- | --- | --- | +| Message fetched via `MessagesRequest` | Authoritative | Render the state directly. | +| Message that arrived live over the websocket | `false` | Means **"not reported"**, not "unsubscribed". | +| Message you constructed locally | `false` | Not yet known. | - + -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. +A `false` on a message that was **not** fetched with the flag means "the server did not tell me", not "the user is unsubscribed". Only a fetched flag is authoritative. Render the unsubscribed state (an enabled "Subscribe" control) in the unknown case — never a spinner or a disabled control. - + + +The property also has a setter, which is **local only and performs no network call**. It exists so you can align message objects you already hold with a truth you have just established — for example after a successful `subscribeToThread`: + + + +```swift +CometChat.subscribeToThread(parentMessageId: parentMessageId) { _ in + message.threadSubscribed = true // local only; keeps your held objects in step +} onError: { error in + print("Failed to subscribe: \(error.errorDescription)") +} +``` + + + +To change the actual subscription, always use `subscribeToThread` / `unsubscribeFromThread`. ## Fetch the Threads a User Participates In @@ -114,6 +129,8 @@ threadsRequest.fetchNext { threads in 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. +Paging is internally keyed on a compound **`(updatedAt, id)`** cursor, matching the JS and Android SDKs. `updatedAt` alone is second-granular, so threads updated within the same second could not be separated by it and a page boundary could only step past the whole second — skipping every unseen row in it. Carrying the boundary row's id makes the cursor address one exact row, so a block of same-second threads spills across pages instead of being dropped. You do not set this yourself; it matters only if you were previously working around duplicated or skipped rows. + ### The MessageThread Model Each row is a `MessageThread`: @@ -127,9 +144,9 @@ Each row is a `MessageThread`: | `conversationId` | The ID of the conversation the thread belongs to. | | `receiverType` | `user` or `group`. | | `receiverUid` | The raw `UID`/`GUID` of the conversation. Resolve the display name and avatar yourself via `CometChat.getUser()` / `CometChat.getGroup()`. | -| `subscriptionState` | Always `SUBSCRIBED` for rows in this list. | +| `isSubscribed` | Always `true` for rows in this list — presence in the list *is* the subscription. | | `unreadReplyCount` | Reserved for future use — currently `nil` (unknown), which is not the same as `0`. | -| `updatedAt` | An internal pagination cursor. **Do not sort your UI on it.** | +| `updatedAt` | Part of the internal pagination cursor (paired with the row's id). Second-granular. **Do not sort your UI on it.** | @@ -143,44 +160,27 @@ The list starts **empty** for every user when the feature launches — it fills -## Real-time Thread Events - -Conform to `CometChatThreadDelegate` to keep your UI in sync as subscription state changes and replies arrive. +## Keeping Your UI in Sync - - -```swift -CometChat.threadDelegate = self +The SDK does **not** expose a thread listener or thread-subscription callbacks. It deliberately does not cache subscription state: the state it could not fully observe drifted, so the server's flag on the message is the single source of truth. -extension ViewController: CometChatThreadDelegate { +Keep your UI in step yourself: - func onThreadSubscriptionChanged(event: ThreadSubscriptionEvent) { - print("Thread \(event.parentMessageId) is now \(event.subscriptionState)") - } +- **After your own subscribe/unsubscribe** — update the message you hold by setting `threadSubscribed` in the success callback (local only, no network). This is the normal case and needs nothing else. +- **On the next fetch** — `threadSubscribed` arrives with every fetched message, so a refresh always corrects the state. +- **For new replies** — use the regular message listener (`onTextMessageReceived` and friends) and check `parentMessageId` to spot a threaded reply. - func onThreadReplyReceived(event: ThreadReplyEvent) { - print("New reply in thread \(event.parentMessageId): \(event.reply.id)") - } -} -``` - - - -Both callbacks are optional. The events carry: + -| Event | Properties | -| ------------------------- | --------------------------------------------------------------------- | -| `ThreadSubscriptionEvent` | `parentMessageId`, `subscriptionState`, `source` | -| `ThreadReplyEvent` | `parentMessageId`, `reply`, `conversationId`, `source` | +A subscribe or unsubscribe performed on the user's **other device** produces no real-time event on this one. The state self-corrects on the next message fetch, so refresh when the app returns to the foreground. -- `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. + - + -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. +Do not build a long-lived local cache of subscription state keyed by parent message id. That is exactly the design the SDK moved away from — it cannot observe every change, so it drifts. Read `threadSubscribed` off the message each time you render. - + ## Notification Preferences @@ -193,6 +193,31 @@ The notification preference for replies gains a new value so users can be notifi | `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. | +### Quoted replies are a separate preference + +A **quoted reply** (a reply to one specific message) and a **threaded reply** (a message posted into a thread) are configured independently, through two different enums. They are not interchangeable — the raw value `4` means something different on each. + +| Enum | Set with | Fourth option | +| --- | --- | --- | +| `RepliesOptions` | `set(repliesPreference:)` | `SUBSCRIBE_TO_SUBSCRIBED_THREADS` | +| `QuotedRepliesOptions` | `set(quotedRepliesPreference:)` | `SUBSCRIBE_TO_QUOTES_ON_OWN_MESSAGES` | + + + +```swift +let groupPreferences = CometChatNotifications.GroupPreferences() + +// Threaded replies: only threads this user follows. +groupPreferences.set(repliesPreference: .SUBSCRIBE_TO_SUBSCRIBED_THREADS) + +// Quoted replies: only quotes of this user's own messages. +groupPreferences.set(quotedRepliesPreference: .SUBSCRIBE_TO_QUOTES_ON_OWN_MESSAGES) +``` + + + +Both preferences exist on group and one-on-one preferences alike. + See [Notification Preferences](/notifications) for how to read and update a user's preferences. ## Error Handling diff --git a/ui-kit/ios/core-features.mdx b/ui-kit/ios/core-features.mdx index 615977623..a420bdbf3 100644 --- a/ui-kit/ios/core-features.mdx +++ b/ui-kit/ios/core-features.mdx @@ -583,9 +583,12 @@ class ThreadSubscriptionViewController: UIViewController { messageListView.enableThreadSubscription = true } - // Read the current state synchronously — no network call - func isSubscribed(parentMessageId: Int) -> Bool { - CometChat.threadSubscriptionState(forParentMessageId: parentMessageId) == .SUBSCRIBED +} + +// Stay in step with a toggle made anywhere in the kit. +extension ViewController: CometChatThreadEventListener { + func ccThreadSubscriptionChanged(parentMessageId: Int, isSubscribed: Bool) { + // update your own control, if you draw one } } ``` diff --git a/ui-kit/ios/guide-thread-subscription.mdx b/ui-kit/ios/guide-thread-subscription.mdx index 636409445..2166a8df6 100644 --- a/ui-kit/ios/guide-thread-subscription.mdx +++ b/ui-kit/ios/guide-thread-subscription.mdx @@ -45,7 +45,7 @@ A subscription is always rooted at the thread's parent message. Offering the opt -Unlike some other CometChat platforms, iOS offers thread subscription in **one-on-one conversations as well as groups** — there is no receiver-type check. +Thread subscription is offered in **one-on-one conversations as well as groups** — there is no receiver-type check. Subscribing applies to a 1-1 thread, and unsubscribing genuinely suppresses its notifications. @@ -59,12 +59,14 @@ To replace the kit's behavior with your own, supply an `onItemClick` on a custom ## Surface 2: The Bell in the Message Header -[CometChatMessageHeader](/ui-kit/ios/message-header) renders a subscribe/unsubscribe bell in its trailing area. Set `parentMessage` to put the header in **thread mode** — the bell renders only then, so a conversation header is unaffected. +[CometChatMessageHeader](/ui-kit/ios/message-header) renders a subscribe/unsubscribe bell in its trailing area. Set `parentMessage` to put the header in **thread mode** — the bell renders only then, so a conversation header is unaffected. As with the action-sheet option, it renders in one-on-one threads as well as groups. + +[CometChatThreadedMessageHeader](/ui-kit/ios/threaded-messages-header) can render the same bell on its reply-count bar instead, for a screen with no header bar. Enable it on exactly one of the two — never both. ```swift let messageHeaderView = CometChatMessageHeader() messageHeaderView.set(parentMessage: parentMessage) // puts the header in thread mode -messageHeaderView.enableThreadSubscription = true // off by default +messageHeaderView.set(enableThreadSubscription: true) // off by default ``` The bell tracks state on its own: it reads the current subscription state from the SDK, flips optimistically on tap, reverts if the request fails, toasts in both directions, and emits `ccThreadSubscriptionChanged` on success. You do not wire any of that up. @@ -72,12 +74,12 @@ The bell tracks state on its own: it reads the current subscription state from t If your screen already draws its own control and you would otherwise show two bells, suppress the kit's: ```swift -messageHeaderView.hideThreadSubscriptionButton = true +messageHeaderView.set(hideThreadSubscriptionButton: true) ``` -The bell needs **both** `parentMessage` and `enableThreadSubscription`. Setting the flag alone on a conversation header renders nothing — that is deliberate, so turning the feature on globally cannot put a thread control on a non-thread screen. +The bell needs `parentMessage` and `enableThreadSubscription`, on a parent whose message has been sent. Setting the flag alone on a conversation header renders nothing — that is deliberate, so turning the feature on globally cannot put a thread control on a non-thread screen. @@ -90,7 +92,7 @@ CometChatThreadEvents.ccThreadSubscriptionChanged(parentMessageId: parentMessage isSubscribed: !isSubscribed) ``` -`CometChatMessageHeader` observes this itself, so its bell stays correct when the user toggles from the action sheet — no wiring needed. Conform to `CometChatThreadEventListener` only to keep a control of **your own** in step: +Both header components observe this themselves, so the bell stays correct when the user toggles from the action sheet — no wiring needed. Conform to `CometChatThreadEventListener` only to keep a control of **your own** in step: ```swift extension ThreadedMessagesVC: CometChatThreadEventListener { diff --git a/ui-kit/ios/message-bubble-styling.mdx b/ui-kit/ios/message-bubble-styling.mdx index c161ccdf7..aee661bb9 100644 --- a/ui-kit/ios/message-bubble-styling.mdx +++ b/ui-kit/ios/message-bubble-styling.mdx @@ -244,6 +244,8 @@ style.gridSpacing = 4 style.tileCornerRadius = 8 style.overflowOverlayColor = UIColor.black.withAlphaComponent(0.6) style.playIconTint = .white + +CometChatMessageBubble.style.incoming.galleryBubbleStyle = style ``` @@ -299,6 +301,8 @@ var style = GalleryBubbleStyle() style.fileCardBackgroundColor = UIColor(hexString: "#FEEDE1") style.fileNameFont = .systemFont(ofSize: 15, weight: .semibold) style.expandButtonColor = UIColor(hexString: "#F76808") + +CometChatMessageBubble.style.incoming.galleryBubbleStyle = style ``` @@ -314,12 +318,12 @@ Key `GalleryBubbleStyle` properties for file cards and the shared caption row: | `fileMetaFont` / `fileMetaColor` | `UIFont` / `UIColor` | File size/type metadata text | | `expandButtonFont` / `expandButtonColor` | `UIFont` / `UIColor` | The "+N more" / "Show less" toggle | | `sectionSpacing` | `CGFloat` | Vertical spacing between media, audio, files and caption sections | -| `captionFont` / `captionColor` | `UIFont` / `UIColor` | The shared caption row under the attachments | +| `captionFont` / `captionColor` | `UIFont` / `UIColor` | The shared caption row under the attachments. Left unset, `captionColor` follows the text bubble's `textColor` | | `captionDividerColor` | `UIColor` | The divider line above the caption | -Captions containing rich text (code blocks, quote blocks, inline code) render through the same pipeline as [Text Bubbles](#text-bubble), so their formatting looks identical across message types. +Captions containing rich text (code blocks, quote blocks, inline code) render through the same pipeline as [Text Bubbles](#text-bubble), so their formatting looks identical across message types. Captions inherit `textBubbleStyle.textColor`, so recoloring text bubbles recolors captions and their markdown too — set `captionColor` only when captions need to differ. diff --git a/ui-kit/ios/message-header.mdx b/ui-kit/ios/message-header.mdx index b806bde5c..a2f450a7f 100644 --- a/ui-kit/ios/message-header.mdx +++ b/ui-kit/ios/message-header.mdx @@ -898,7 +898,7 @@ messageHeader.disableTyping = true ### enableThreadSubscription -Enables the subscribe/unsubscribe bell in the header's trailing area, letting users opt in to notifications for a thread's replies. The bell **also** requires `parentMessage` — setting this flag alone on a conversation header renders nothing, so turning the feature on globally cannot put a thread control on a non-thread screen. +Enables the subscribe/unsubscribe bell in the header's trailing area, letting users opt in to notifications for a thread's replies. The bell **also** requires `parentMessage` — setting this flag alone on a conversation header renders nothing, so turning the feature on globally cannot put a thread control on a non-thread screen. It renders in one-on-one threads as well as groups. The bell manages itself: it reads the current state from the SDK, flips optimistically on tap, reverts on failure, toasts in both directions, and stays in step with a toggle made from the message action sheet. @@ -912,7 +912,7 @@ import CometChatUIKitSwift let messageHeader = CometChatMessageHeader() messageHeader.set(parentMessage: parentMessage) -messageHeader.enableThreadSubscription = true +messageHeader.set(enableThreadSubscription: true) ``` See the [Thread Subscription guide](/ui-kit/ios/guide-thread-subscription) for the full feature. diff --git a/ui-kit/ios/message-list.mdx b/ui-kit/ios/message-list.mdx index 9f441fce2..ec15abfbf 100644 --- a/ui-kit/ios/message-list.mdx +++ b/ui-kit/ios/message-list.mdx @@ -1511,9 +1511,7 @@ messageList.enableSaveMessage = true ### enableThreadSubscription -Enables the **Subscribe to thread** / **Unsubscribe from thread** option in message actions, letting users opt in to notifications for a thread's replies. The option is offered on **parent messages only** — never on a reply inside a thread, because a subscription is always rooted at the thread's parent. It is not gated on reply count: subscribing to a message that has no replies yet is supported. - -Unlike some other CometChat platforms, iOS offers this in one-on-one conversations as well as groups. +Enables the **Subscribe to thread** / **Unsubscribe from thread** option in message actions, letting users opt in to notifications for a thread's replies. The option is offered on **parent messages only** — never on a reply inside a thread, because a subscription is always rooted at the thread's parent. It is not gated on reply count: subscribing to a message that has no replies yet is supported. It renders in one-on-one conversations as well as groups. | | | |---|---| diff --git a/ui-kit/ios/pinned-messages.mdx b/ui-kit/ios/pinned-messages.mdx index 103ed03df..5ba631b6b 100644 --- a/ui-kit/ios/pinned-messages.mdx +++ b/ui-kit/ios/pinned-messages.mdx @@ -411,6 +411,19 @@ The group whose pinned messages to show. Mutually exclusive with `user`; pass it | Type | `Group?` | | Default | `nil` | +### enableThreadSubscription + +Enables the **Subscribe to thread** / **Unsubscribe from thread** option in the pinned row's message actions, exactly as on [MessageList](/ui-kit/ios/message-list). Off by default and opted into per surface — turning it on for the message list does **not** turn it on here. + +| | | +|---|---| +| Type | `Bool` | +| Default | `false` | + +```swift lines +pinnedVC.enableThreadSubscription = true +``` + ### hideUnpinOption Hides the per-row unpin swipe action. Set this for users who cannot pin in this conversation. diff --git a/ui-kit/ios/threaded-messages-header.mdx b/ui-kit/ios/threaded-messages-header.mdx index 6dbdac5c1..51f89def2 100644 --- a/ui-kit/ios/threaded-messages-header.mdx +++ b/ui-kit/ios/threaded-messages-header.mdx @@ -191,6 +191,31 @@ Below is a list of customizations along with corresponding code snippets: | setMaxHeight | Sets the maximum height for the threaded message view. | `setMaxHeight(300)` | | setMessageAlignment | Sets the alignment of messages (e.g., left or right). | `setMessageAlignment(.right)` | | setParentMessage | Sets the parent message for the threaded conversation. | `setParentMessage(parentMessage)` | +| enableThreadSubscription | Shows the subscribe/unsubscribe bell on the reply-count bar. | `set(enableThreadSubscription: true)` | +| hideThreadSubscriptionButton | Hides the bell while leaving the feature on, for a screen that draws its own control. | `set(hideThreadSubscriptionButton: true)` | + +### Thread Subscription + +With `enableThreadSubscription` on, the header renders a subscribe/unsubscribe bell as a trailing action on its reply-count bar. It appears only on a **sent parent message**, in one-on-one threads as well as groups. + +The bell manages itself: it reads the current state from the SDK, flips optimistically on tap, reverts on failure, toasts in both directions, and stays in step with a toggle made from the message action sheet. + + + +Most thread screens show the bell in their header bar via [CometChatMessageHeader](/ui-kit/ios/message-header) instead — that is what the sample apps do, and it matches the other CometChat platforms. Use this component's bell when your screen has no header bar, and enable it on **exactly one** of the two so a screen never shows two bells. + + + +```swift lines +import CometChatUIKitSwift + +let threadHeaderView = CometChatThreadedMessageHeader() +threadHeaderView.set(parentMessage: parentMessage) +threadHeaderView.set(controller: self) +threadHeaderView.set(enableThreadSubscription: true) +``` + +See the [Thread Subscription guide](/ui-kit/ios/guide-thread-subscription) for the full feature. --- From fa037ae45d93a8064d925ba5fbef26ec2650ffec Mon Sep 17 00:00:00 2001 From: shreeyajoshi-cometchat Date: Mon, 7 Sep 2026 21:29:45 +0530 Subject: [PATCH 3/5] docs(ui-kit/ios): pin to UI Kit 5.1.22 and Chat SDK 4.1.9 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both feature PRs ship these versions: uikit-ios#971 (v5.1.22) and chat-sdk-ios#485 (v4.1.9). Updates the six pins in getting-started — the quick-reference table, the Podfile snippet, the SPM exact-version entries and the binary-compatibility warning. CometChatCallsSDK is left at 4.2.3 for now, by request. Note that the repo's own Podfile.lock resolves CometChatCallsSDK 5.0.4, and CometChatPushNotifications requires >= 5.0.0, so that pin is separately stale and worth revisiting. These versions are not on CocoaPods trunk yet — latest published are UI Kit 5.1.18 and Chat SDK 4.1.7 — so hold this page until both releases publish, or `pod install` will fail to resolve. Co-Authored-By: Claude Opus 5 (1M context) --- ui-kit/ios/getting-started.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ui-kit/ios/getting-started.mdx b/ui-kit/ios/getting-started.mdx index 969c5207f..b9abeb260 100644 --- a/ui-kit/ios/getting-started.mdx +++ b/ui-kit/ios/getting-started.mdx @@ -9,14 +9,14 @@ description: "Integrate CometChat iOS UI Kit with project setup, package install | Field | Value | | --- | --- | | Package | `CometChatUIKitSwift` | -| Version | `5.1.18` | +| Version | `5.1.22` | | Requirements | Xcode 16+, iOS 13.0+, Swift 5.0+ | | Init | `CometChatUIKit.init(uiKitSettings:)` — must complete before `login()` | | Login | `CometChatUIKit.login(uid:)` — must complete before rendering components | | Order | `init()` → `login()` → render. Breaking this order = blank screen | | Auth Key | Dev/testing only. Use Auth Token in production | | Calling | Optional. Install `CometChatCallsSDK 4.2.3` to enable | -| Dependencies | `CometChatSDK 4.1.7` (installed automatically) | +| Dependencies | `CometChatSDK 4.1.9` (installed automatically) | | AI Skills | `npx @cometchat/skills add` — [GitHub](https://github.com/cometchat/cometchat-skills) | @@ -103,7 +103,7 @@ platform :ios, '13.0' use_frameworks! target 'YourApp' do - pod 'CometChatUIKitSwift', '5.1.18' + pod 'CometChatUIKitSwift', '5.1.22' # Optional: Voice/Video Calling pod 'CometChatCallsSDK', '4.2.3' @@ -135,7 +135,7 @@ https://github.com/cometchat/cometchat-uikit-ios 3. Select **Up to Exact Version** and enter: ``` -5.1.18 +5.1.22 ``` 4. Add the Chat SDK separately using: @@ -144,12 +144,12 @@ https://github.com/cometchat/cometchat-uikit-ios https://github.com/cometchat/chat-sdk-ios ``` - Exact Version: `4.1.7` + Exact Version: `4.1.9` -**Pin the Chat SDK to the exact version the UI Kit requires.** `CometChatUIKitSwift` ships as a prebuilt binary compiled against one specific `CometChatSDK` version — for UI Kit `5.1.18` that is `CometChatSDK 4.1.7` (see the version table at the top of this page). +**Pin the Chat SDK to the exact version the UI Kit requires.** `CometChatUIKitSwift` ships as a prebuilt binary compiled against one specific `CometChatSDK` version — for UI Kit `5.1.22` that is `CometChatSDK 4.1.9` (see the version table at the top of this page). - **CocoaPods** installs the matching `CometChatSDK` automatically when you run `pod install`. Don't add your own `CometChatSDK` pod pinned to a different version. - **Swift Package Manager** does not resolve it for you — add `chat-sdk-ios` with **Up to Exact Version** set to the version the UI Kit depends on. Forcing a newer or older Chat SDK (an SPM override) is unsupported and typically fails to build or crashes at runtime. From dbd60bd628c8eb20df4aeb0a729b89dc7991594a Mon Sep 17 00:00:00 2001 From: shreeyajoshi-cometchat Date: Mon, 7 Sep 2026 21:38:32 +0530 Subject: [PATCH 4/5] docs(sdk/ios): stop using "pinned" as a tags example Three tag examples used "pinned" as a free-form tag. With pin/save messages and pinned conversations now real features, those read as the feature rather than as an arbitrary label. Swapped for neutral tags and added a pointer to the actual API in each place: pin-save-message for messages, pinConversation and set(pinnedBy:) for conversations. Completes the sweep started in a1b76a5c, which caught additional-message-filtering and send-message but missed these. Co-Authored-By: Claude Opus 5 (1M context) --- sdk/ios/edit-message.mdx | 6 ++++-- sdk/ios/retrieve-conversations.mdx | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/sdk/ios/edit-message.mdx b/sdk/ios/edit-message.mdx index bd742783f..7924d4507 100644 --- a/sdk/ios/edit-message.mdx +++ b/sdk/ios/edit-message.mdx @@ -49,9 +49,11 @@ The `edit()` method returns a [`BaseMessage`](/sdk/reference/messages#basemessag Use `tags` to update tags when editing. New tags replace existing ones. +Tags are your own free-form labels and are unrelated to pinning a message — to pin one, use [Pin & Save Messages](/sdk/ios/pin-save-message). + ```swift -let tags = ["pinned", "important"] -let textMessage = TextMessage(receiverUid: "cometchat-uid-2", text: "Pinned message", receiverType: .user) +let tags = ["important", "follow-up"] +let textMessage = TextMessage(receiverUid: "cometchat-uid-2", text: "Tagged message", receiverType: .user) textMessage.id = 12345 textMessage.tags = tags diff --git a/sdk/ios/retrieve-conversations.mdx b/sdk/ios/retrieve-conversations.mdx index badeedd0d..5d129cb7d 100644 --- a/sdk/ios/retrieve-conversations.mdx +++ b/sdk/ios/retrieve-conversations.mdx @@ -105,8 +105,10 @@ var conversationRequest = ConversationRequest.ConversationRequestBuilder(limit: Fetch conversations that have specific tags. +Tags are your own free-form labels. Pinning a conversation is a separate feature — use `pinConversation` and `ConversationRequestBuilder.set(pinnedBy:)` rather than a `"pinned"` tag. + ```swift -let tags = ["pinned", "archived"] +let tags = ["priority", "archived"] var conversationRequest = ConversationRequest.ConversationRequestBuilder(limit: 50) .setTags(tags) .build() @@ -209,7 +211,7 @@ Use `tagConversation()` to add tags to a conversation. ```swift let id = "cometchat-uid-1" -let tags = ["pinned"] +let tags = ["priority"] CometChat.tagConversation(conversationWith: id, conversationType: .user, tags: tags, onSuccess: { conversation in print("conversation", conversation) From 929a7ec54af1a4043a9798d2d53cac7854507566 Mon Sep 17 00:00:00 2001 From: shreeyajoshi-cometchat Date: Wed, 9 Sep 2026 20:53:07 +0530 Subject: [PATCH 5/5] docs(ui-kit/ios,sdk/ios): state the minimum versions for pin, save and threads Review feedback on #465: none of the new pages said which release introduced the feature. New pages get a callout at the top; sections added to existing pages get an inline "Available since" marker rather than a page-level note, so the version claim is scoped to the new API and not the whole page. UI Kit components and features ship in 5.1.22, which pairs with Chat SDK 4.1.9; the SDK APIs need 4.1.9. Also corrects two prerequisites that were wrong or unhelpfully vague: guide-thread-subscription asked for "Chat SDK v5 or later", a version that does not exist on iOS, and guide-pin-save-message asked for "UIKit v5+", which would let a reader install an older 5.1.x and not find the feature. Co-Authored-By: Claude Opus 5 (1M context) --- sdk/ios/all-real-time-delegates-listeners.mdx | 8 ++++---- sdk/ios/pin-save-message.mdx | 4 ++++ sdk/ios/thread-subscription.mdx | 2 ++ ui-kit/ios/core-features.mdx | 6 ++++++ ui-kit/ios/events.mdx | 2 ++ ui-kit/ios/guide-pin-save-message.mdx | 6 +++++- ui-kit/ios/guide-thread-subscription.mdx | 6 +++++- ui-kit/ios/message-header.mdx | 6 ++++++ ui-kit/ios/message-list.mdx | 12 ++++++++++++ ui-kit/ios/pinned-messages.mdx | 4 ++++ ui-kit/ios/saved-messages.mdx | 4 ++++ ui-kit/ios/threaded-messages-header.mdx | 2 ++ 12 files changed, 56 insertions(+), 6 deletions(-) diff --git a/sdk/ios/all-real-time-delegates-listeners.mdx b/sdk/ios/all-real-time-delegates-listeners.mdx index efbe9fd04..8bd1f8358 100644 --- a/sdk/ios/all-real-time-delegates-listeners.mdx +++ b/sdk/ios/all-real-time-delegates-listeners.mdx @@ -212,10 +212,10 @@ The `CometChatMessageDelegate` provides you with live events related to messages | **onInteractionGoalCompleted(_ receipt: InteractionReceipt)**| This event is triggered when an interaction Goal is achieved. | | **onMessageReactionAdded(reactionEvent: ReactionEvent)** | This event is triggered when a reaction is added to a message. | | **onMessageReactionRemoved(reactionEvent: ReactionEvent)** | This event is triggered when a reaction is removed from a message. | -| **onMessagePinned(message:** [`BaseMessage`](/sdk/reference/messages#basemessage)**)** | This event is triggered when a message is pinned. Broadcast to everyone in the conversation. See [Pin & Save Messages](/sdk/ios/pin-save-message). | -| **onMessageUnpinned(message:** [`BaseMessage`](/sdk/reference/messages#basemessage)**)** | This event is triggered when a message is unpinned. Broadcast to everyone in the conversation. | -| **onMessageSaved(message:** [`BaseMessage`](/sdk/reference/messages#basemessage)**)** | This event is triggered when a message is saved. Private — reaches only the saving user's other devices. | -| **onMessageUnsaved(message:** [`BaseMessage`](/sdk/reference/messages#basemessage)**)** | This event is triggered when a message is unsaved. Private — reaches only the saving user's other devices. | +| **onMessagePinned(message:** [`BaseMessage`](/sdk/reference/messages#basemessage)**)** | *Available since v4.1.9.* This event is triggered when a message is pinned. Broadcast to everyone in the conversation. See [Pin & Save Messages](/sdk/ios/pin-save-message). | +| **onMessageUnpinned(message:** [`BaseMessage`](/sdk/reference/messages#basemessage)**)** | *Available since v4.1.9.* This event is triggered when a message is unpinned. Broadcast to everyone in the conversation. | +| **onMessageSaved(message:** [`BaseMessage`](/sdk/reference/messages#basemessage)**)** | *Available since v4.1.9.* This event is triggered when a message is saved. Private — reaches only the saving user's other devices. | +| **onMessageUnsaved(message:** [`BaseMessage`](/sdk/reference/messages#basemessage)**)** | *Available since v4.1.9.* This event is triggered when a message is unsaved. Private — reaches only the saving user's other devices. | | **onMessageModerated(_ message:** [`BaseMessage`](/sdk/reference/messages#basemessage)**)** | This event is triggered when a message is moderated. | | **onAIAssistantMessageReceived(_ message: AIAssistantMessage)** | This event is triggered when an AI Assistant message is received. | | **onAIToolResultMessageReceived(_ message: AIToolResultMessage)** | This event is triggered when an AI Tool result message is received. | diff --git a/sdk/ios/pin-save-message.mdx b/sdk/ios/pin-save-message.mdx index 9c3d16443..6724d6c23 100644 --- a/sdk/ios/pin-save-message.mdx +++ b/sdk/ios/pin-save-message.mdx @@ -4,6 +4,10 @@ sidebarTitle: "Pin & Save Messages" description: "Pin messages for everyone in a conversation and save messages privately using the CometChat iOS SDK, including message fields, fetching, and listener events." --- + +**Available from Chat SDK v4.1.9.** These APIs require `CometChatSDK` v4.1.9 or later. + + ```swift diff --git a/sdk/ios/thread-subscription.mdx b/sdk/ios/thread-subscription.mdx index e4eeb1d34..312a33f67 100644 --- a/sdk/ios/thread-subscription.mdx +++ b/sdk/ios/thread-subscription.mdx @@ -6,6 +6,8 @@ Give users Slack-style control over thread noise. A user can **subscribe** to a +**Available from Chat SDK v4.1.9.** These APIs require `CometChatSDK` v4.1.9 or later. + Thread subscription builds on [Threaded Messages](/sdk/ios/threaded-messages). A thread is identified by the ID of its **parent message** — there is no separate thread ID. diff --git a/ui-kit/ios/core-features.mdx b/ui-kit/ios/core-features.mdx index a420bdbf3..a6c240869 100644 --- a/ui-kit/ios/core-features.mdx +++ b/ui-kit/ios/core-features.mdx @@ -563,6 +563,8 @@ class ThreadedMessagesViewController: UIViewController { ## Thread Subscription +*Available since v5.1.22* + Let users subscribe to a thread to be notified about its replies, or unsubscribe to mute it. Users are subscribed automatically when they start a thread, reply in one, or are @-mentioned in one — subscribing explicitly is how they opt in to a thread they haven't participated in yet. ```swift lines @@ -712,6 +714,8 @@ class QuotedReplyViewController: UIViewController { ## Pin Message +*Available since v5.1.22* + Pin an important message so everyone in the conversation can find it. Pinning is restricted to group owners, admins and moderators; everyone else sees the pin indicator and the pinned list. ```swift lines @@ -745,6 +749,8 @@ See the [Pin & Save Messages guide](/ui-kit/ios/guide-pin-save-message) and [Pin ## Save Message +*Available since v5.1.22* + Save a message privately for later. Saves are visible only to the user who made them and span every conversation, so they are listed on a user-level screen rather than a per-conversation one. ```swift lines diff --git a/ui-kit/ios/events.mdx b/ui-kit/ios/events.mdx index 9775981b2..f7c01fd7d 100644 --- a/ui-kit/ios/events.mdx +++ b/ui-kit/ios/events.mdx @@ -362,6 +362,8 @@ CometChatConversationEvents.ccUpdateConversation(conversation: updatedConversati ## Thread Events +*Available since v5.1.22* + Listen for thread subscription changes so any subscription control you host stays in agreement with the kit's action-sheet option, without a refetch. ```swift lines diff --git a/ui-kit/ios/guide-pin-save-message.mdx b/ui-kit/ios/guide-pin-save-message.mdx index 9b1ad8dbb..70bafd9f9 100644 --- a/ui-kit/ios/guide-pin-save-message.mdx +++ b/ui-kit/ios/guide-pin-save-message.mdx @@ -6,6 +6,10 @@ description: "Enable pin and save messages in the CometChat iOS UI Kit, wire the Let users mark messages for later: **pin** a message so everyone in the conversation sees it, or **save** one privately for yourself. + +**Available from UI Kit v5.1.22.** This feature requires `CometChatUIKitSwift` v5.1.22 or later, which depends on `CometChatSDK` v4.1.9. + + ## Overview Pin and save look alike in the action sheet but behave differently: @@ -23,7 +27,7 @@ With the iOS UI Kit you get: the action-sheet options, pin and bookmark indicato ## Prerequisites 1. Completed [Getting Started](/ui-kit/ios/getting-started) setup -2. CometChat UIKit v5+ installed +2. CometChat UI Kit for iOS v5.1.22 or later installed 3. User logged in with `CometChatUIKit.login()` 4. Pin and save enabled for your app diff --git a/ui-kit/ios/guide-thread-subscription.mdx b/ui-kit/ios/guide-thread-subscription.mdx index 2166a8df6..6741a7082 100644 --- a/ui-kit/ios/guide-thread-subscription.mdx +++ b/ui-kit/ios/guide-thread-subscription.mdx @@ -4,6 +4,10 @@ sidebarTitle: "Thread Subscription" description: "Let users subscribe to or unsubscribe from message threads in the CometChat iOS UI Kit so notifications only reach the people who care." --- + +**Available from UI Kit v5.1.22.** This feature requires `CometChatUIKitSwift` v5.1.22 or later, which depends on `CometChatSDK` v4.1.9. + + ## 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. @@ -13,7 +17,7 @@ The UI Kit ships the toggle as a **Subscribe to thread / Unsubscribe from thread ## Prerequisites - Threaded messages working in your app — see [Threaded Messages](/ui-kit/ios/guide-threaded-messages). -- CometChat UI Kit for iOS with Chat SDK v5 or later. +- CometChat UI Kit for iOS v5.1.22 or later, with Chat SDK v4.1.9 or later. ## Enable the Feature diff --git a/ui-kit/ios/message-header.mdx b/ui-kit/ios/message-header.mdx index a2f450a7f..daa6d5563 100644 --- a/ui-kit/ios/message-header.mdx +++ b/ui-kit/ios/message-header.mdx @@ -898,6 +898,8 @@ messageHeader.disableTyping = true ### enableThreadSubscription +*Available since v5.1.22* + Enables the subscribe/unsubscribe bell in the header's trailing area, letting users opt in to notifications for a thread's replies. The bell **also** requires `parentMessage` — setting this flag alone on a conversation header renders nothing, so turning the feature on globally cannot put a thread control on a non-thread screen. It renders in one-on-one threads as well as groups. The bell manages itself: it reads the current state from the SDK, flips optimistically on tap, reverts on failure, toasts in both directions, and stays in step with a toggle made from the message action sheet. @@ -969,6 +971,8 @@ Hides the user status (online/offline/last active). ### hideThreadSubscriptionButton +*Available since v5.1.22* + Hides the subscribe/unsubscribe bell while leaving the feature on — for a screen that already draws its own control and would otherwise show two. | | | @@ -1036,6 +1040,8 @@ messageHeader.set(onAiNewChatClicked: { [weak self] in ### parentMessage +*Available since v5.1.22* + The thread's root message the subscription bell acts on. Setting it puts the header in **thread mode**; leaving it `nil` keeps the header in its normal conversation mode with no bell. | | | diff --git a/ui-kit/ios/message-list.mdx b/ui-kit/ios/message-list.mdx index ec15abfbf..358b6552c 100644 --- a/ui-kit/ios/message-list.mdx +++ b/ui-kit/ios/message-list.mdx @@ -1479,6 +1479,8 @@ Enables AI-powered conversation summary feature. ### enablePinMessage +*Available since v5.1.22* + Enables the pin and unpin options in message actions. The feature must **also** be enabled for your app — the kit checks `CometChat.isPinMessageEnabled()` — and pinning is further gated by the user's scope. | | | @@ -1495,6 +1497,8 @@ messageList.enablePinMessage = true ### enableSaveMessage +*Available since v5.1.22* + Enables the save and unsave options in message actions. The feature must **also** be enabled for your app — the kit checks `CometChat.isSaveMessageEnabled()`. | | | @@ -1511,6 +1515,8 @@ messageList.enableSaveMessage = true ### enableThreadSubscription +*Available since v5.1.22* + Enables the **Subscribe to thread** / **Unsubscribe from thread** option in message actions, letting users opt in to notifications for a thread's replies. The option is offered on **parent messages only** — never on a reply inside a thread, because a subscription is always rooted at the thread's parent. It is not gated on reply count: subscribing to a message that has no replies yet is supported. It renders in one-on-one conversations as well as groups. | | | @@ -1728,6 +1734,8 @@ Hides the new message indicator. ### hidePinMessageOption +*Available since v5.1.22* + Hides the pin option in message actions. Independent of `enablePinMessage` — use this to suppress the option on one screen while the feature stays on elsewhere. | | | @@ -1780,6 +1788,8 @@ messageList.hideReplyMessageOption = true ### hideSaveMessageOption +*Available since v5.1.22* + Hides the save option in message actions. Independent of `enableSaveMessage` — use this to suppress the option on one screen while the feature stays on elsewhere. | | | @@ -1796,6 +1806,8 @@ messageList.hideSaveMessageOption = true ### hideThreadSubscriptionOption +*Available since v5.1.22* + Hides the subscribe/unsubscribe option in message actions. Independent of `enableThreadSubscription` — use this to suppress the option on one screen while the feature stays on elsewhere. Ignored while `enableThreadSubscription` is `false`, since nothing renders in that case anyway. | | | diff --git a/ui-kit/ios/pinned-messages.mdx b/ui-kit/ios/pinned-messages.mdx index 5ba631b6b..06494025c 100644 --- a/ui-kit/ios/pinned-messages.mdx +++ b/ui-kit/ios/pinned-messages.mdx @@ -5,6 +5,10 @@ description: "Display and manage CometChat iOS UI Kit pinned messages for a conv `CometChatPinnedMessages` is a full-screen list of the messages pinned in a single conversation, newest pin first. Pins are conversation-wide: everyone in the chat sees the same list, and moderators can unpin from any row. + +**Available from UI Kit v5.1.22.** These components require `CometChatUIKitSwift` v5.1.22 or later, which depends on `CometChatSDK` v4.1.9. + + ```json { diff --git a/ui-kit/ios/saved-messages.mdx b/ui-kit/ios/saved-messages.mdx index 4c59ccb1c..abe4daf2e 100644 --- a/ui-kit/ios/saved-messages.mdx +++ b/ui-kit/ios/saved-messages.mdx @@ -5,6 +5,10 @@ description: "Display CometChat iOS UI Kit saved messages for the logged-in user `CometChatSavedMessages` is a full-screen list of the messages the logged-in user has saved, newest save first. Saves are **private to the user** and **span every conversation**, so each row is labelled with the chat it came from. + +**Available from UI Kit v5.1.22.** These components require `CometChatUIKitSwift` v5.1.22 or later, which depends on `CometChatSDK` v4.1.9. + + ```json { diff --git a/ui-kit/ios/threaded-messages-header.mdx b/ui-kit/ios/threaded-messages-header.mdx index 51f89def2..6e3943036 100644 --- a/ui-kit/ios/threaded-messages-header.mdx +++ b/ui-kit/ios/threaded-messages-header.mdx @@ -196,6 +196,8 @@ Below is a list of customizations along with corresponding code snippets: ### Thread Subscription +*Available since v5.1.22* + With `enableThreadSubscription` on, the header renders a subscribe/unsubscribe bell as a trailing action on its reply-count bar. It appears only on a **sent parent message**, in one-on-one threads as well as groups. The bell manages itself: it reads the current state from the SDK, flips optimistically on tap, reverts on failure, toasts in both directions, and stays in step with a toggle made from the message action sheet.