docs(ui-kit/angular): thread subscription, pin & save messages and conversations - #495
Conversation
…nversations
Documents the Angular UI Kit work in uikit-angular#559 — thread follow/unfollow,
pin and save for messages, and conversation pinning.
New pages
components/cometchat-pinned-messages the per-conversation pinned panel
components/cometchat-saved-messages the per-user saved view
guides/thread-subscription following a thread end to end
guides/pin-and-save-messages enabling the features, limits, indicators
Updated
events.mdx CometChatThreadEvents and CometChatPinSaveEvents,
both tiers and the merged observables
customization/global-config.mdx enableThreadSubscription, enablePinMessage,
enableSaveMessage
core-features.mdx the app settings that gate all of it
message-list, message-bubble, message-header, conversations,
conversation-item, thread-header the new inputs and outputs
guides/threaded-messages, guides/custom-text-formatter, message-composer
Written against the branch rather than a spec, which caught several things a
read of the design would have got wrong: the thread payload is
{ parentMessageId, subscribed } and there is no `state` or `source` field; the
optimistic tier (ccMessagePinChanged / ccMessageSaveChanged) and the merged
pinned$/unpinned$/saved$/unsaved$ are what a surface should subscribe to rather
than the raw subjects; ThreadSubscriptionService takes the message, not an id;
and the toggle is leading-edge — the write leaves on the first tap and anything
within 400ms is swallowed, not collapsed into a trailing call.
Conversation pins carry two reaches and the docs separate them: a personal pin
is private and multi-device, while an admin pin is app-wide
(`pinnedBy === "app_system"`), distinguished by `isSystemPinned()`.
Both new component pages carry a live Storybook preview, matching the pattern
the neighbouring pages already use. All 44 story ids referenced across
ui-kit/angular resolve against storybook.cometchat.io/angular.
Branched from current main rather than continuing the earlier branch, which had
fallen 53 commits behind. Content is identical to it; only docs.json differs,
rebuilt on main's newer navigation.
Verified: docs.json parses and every ui-kit/angular nav entry resolves to a
file; the site builds and serves locally, both new pages included.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018rhePm8oMBbjPeRFzN7HTg
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
Docs PR review — structural checks clean, content has real driftVerdict: request changes. Nothing here breaks the build or a live URL — the mechanical risk profile of this PR is about as good as it gets. The problems are all in step 5, content integrity: several passages were written against the design of the feature rather than the branch, and they now contradict both the code and other pages inside this same PR. Three code examples would not compile or would log I verified every API claim against ✅ What passed (actually checked, not assumed)
Also verified correct against source: the whole P0 — must fix before merge1. Three mutually contradictory permission stories for Pin/Unpin, and none matches the codeThis is the headline. The PR states the same rule three different ways:
The implementation has neither. if (!this.hideUnpinMessageOption && !this.isSystemPin(message)) {That is the entire gate — no scope check anywhere in the component ( Same for the message list — The 2.
|
| Documented | Reality |
|---|---|
thread_mute — "Mute thread" |
❌ → thread_subscription_subscribe = "Subscribe to thread" |
thread_unmute — "Unmute thread" |
❌ → thread_subscription_unsubscribe = "Unsubscribe from thread" |
thread_followed_toast |
❌ → thread_subscription_subscribed_toast = "Subscribed. You'll be notified about new replies in this thread." |
thread_unfollowed_toast |
❌ → thread_subscription_unsubscribed_toast = "Unsubscribed. Notifications are off until you reply or are mentioned." |
message_list_option_follow_thread |
❌ does not exist |
message_list_option_unfollow_thread |
❌ does not exist |
thread_subscription_failed |
✅ |
thread_unavailable |
✅ |
An integrator overriding these gets silence. (pin-and-save-messages.mdx's 21 keys, by contrast, all exist — see P1.8 for their values.)
P1 — should fix before merge
6. SDK minimum version is wrong, and pin/save states none at all
guides/thread-subscription.mdx:16 — "A Chat SDK build exposing CometChat.subscribeToThread (4.1.14-beta-1 or later)".
Checked the npm registry: no 4.1.14-beta-1 was ever published (the only prereleases are 4.0.0-beta1/beta2, 4.0.4-beta1). And the released 4.1.14 (2026-09-04) contains zero occurrences of subscribeToThread, pinMessage, isPinMessageEnabled, setPinned, isSystemPinned — none of it. All of it lands in 4.2.0 (published 2026-09-07, ~an hour after this PR opened).
This matters because the kit's peer range is ^4.1.13, so npm i can legitimately resolve to a build where every one of these surfaces silently hides itself with no error to diagnose. Please say 4.2.0 or later, and add the same requirement to guides/pin-and-save-messages.mdx, which currently states no SDK version at all.
7. Every UI label for thread subscription is wrong, including the claim that the two surfaces differ
guides/thread-subscription.mdx:33-34 presents the two surfaces as deliberately labelled differently — "State-labelled — Mute thread / Unmute thread" vs "Action-labelled — Notify me about replies / Stop reply notifications" — and components/cometchat-thread-header.mdx:200 builds a <Note> on that contrast. components/cometchat-message-list.mdx:397 repeats the action-labelled pair.
Both surfaces use the same two strings: thread_subscription_subscribe / thread_subscription_unsubscribe → "Subscribe to thread" / "Unsubscribe from thread". cometchat-message-list.option-builders.ts:231 even says why: "uses the same wording as the thread header's control so the two surfaces read alike." The design rationale in the docs is the inverse of the shipped one.
8. Three limit-error strings are wrong — and contradict the same page's prose
guides/pin-and-save-messages.mdx:354-356 vs en-us/translation.json:
| Key | Documented | Actual |
|---|---|---|
pin_message_limit_error |
"You can pin up to {limit} messages in a conversation." | "You can only pin {limit} messages. Unpin one to pin another." |
save_message_limit_error |
"You can save up to {limit} messages." | "You can only save {limit} messages. Unsave one to save another." |
conversation_pin_limit_error |
"You can pin up to {limit} conversations." | "You can only pin {limit} chats. Unpin one to pin another." |
Note :135 on the same page quotes the correct copy ("You can only pin 5 messages. Unpin one to pin another."), and :231 quotes the wrong one. components/cometchat-conversations.mdx gets it right.
9. messageOptionClick forwards more than documented
components/cometchat-pinned-messages.mdx:133,219 say only Translate and Report are forwarded. cometchat-pinned-messages.component.ts:731-740 also emits messageOptionClick for sendMessagePrivately (alongside firing CometChatUIEvents.ccOpenChat). A host that switches on event.option.id expecting two ids will get a third.
10. Seven CSS-variable defaults are wrong
components/cometchat-pinned-messages.mdx:274-282:
| Variable | Documented | Actual |
|---|---|---|
--cometchat-pinned-messages-width |
400px |
100% |
--cometchat-pinned-messages-title-font |
--cometchat-font-heading4-medium |
--cometchat-font-heading3-bold |
--cometchat-pinned-messages-item-padding |
8px 16px |
8px 8px |
--cometchat-pinned-messages-item-background-hover |
--cometchat-background-color-02 |
--cometchat-extended-primary-color-100 |
--cometchat-pinned-messages-entry-name-font |
--cometchat-font-caption1-medium |
--cometchat-font-body-medium |
components/cometchat-saved-messages.mdx:192,196:
| Variable | Documented | Actual |
|---|---|---|
--cometchat-saved-messages-title-font |
--cometchat-font-heading4-medium |
--cometchat-font-heading3-bold |
--cometchat-saved-messages-row-background-hover |
--cometchat-background-color-02 |
--cometchat-extended-primary-color-100 |
The width one is worth a second look: the CSS carries a comment explaining 400px was deliberately abandoned ("A fixed 400px stayed 400px when the drawer was dragged wider… The host owns the width"), so the docs are documenting the behaviour that was removed.
P2 / nits
guides/pin-and-save-messages.mdx:231says the conversation pin cap comes "with the real number from the rejection", whilecomponents/cometchat-conversations.mdxsays it's read viaCometChat.getPinnedConversationsLimit(). The latter matchescometchat-conversations.component.ts:556. Worth aligning.- The header overflow entry uses localization key
pinned_messages_menu, which isn't in the guide's localization table. - Pre-existing, unrelated, but visible in the
guides-overview.mdxdiff context: the Search Messages row links to/ui-kit/angular/guides/threaded-messages. Easy drive-by fix.
Note on scope
Findings 1 (partly), 10, and the quickOptionsCount default were inherited from docs/components/*.mdx on the source branch rather than authored here — but they ship to readers from this PR, so they belong in this review. Credit where due: this PR corrected the upstream quickOptionsCount default (2 → 1, matching numberAttribute quickOptionsCount = 1), and the itemView/textFormatters/hideMessagePrivatelyOption rows added here are all accurate.
Happy to push the exact edits for any of the above — say the word and I'll open a follow-up branch.
🤖 Review generated with Claude Code
… against the branch Addresses the review on #495. Every change was verified against cometchat-team/uikit-angular@ENG-37617-thread-subscription-follow-unfollow, the shipped en-us/translation.json, the component CSS, and the npm registry. Permissions (the headline): the PR stated the pin/unpin rule three ways — an allow-list on the pinned panel, a deny-list in the guide and the message list — and the implementation has neither. `cometchat-pinned-messages.component.ts:615` gates on `!hideUnpinMessageOption && !isSystemPin()` and nothing else; the only `isParticipant` check in the message-list option path gates Delete. All three passages now say what the code does: no client-side role gate, the server is the sole authority and refuses with ERR_ACTION_NOT_ALLOWED, and the one thing withheld on the client is Unpin on a system pin. Code examples that would not work: - `event.state` -> `event.subscribed` in both samples. IThreadSubscriptionChange is `{ parentMessageId, subscribed }`; the events table on the same page already said so. - dropped the `source` destructure and the paragraph documenting it as an API contract. IPinSaveChanged is `{ message }`. Replaced with the two-tier model (server truth vs this client's optimism) and a pointer at the merged pinned$/unpinned$/saved$/unsaved$ observables. Auto-subscribe: it does not arrive on an SDK thread listener and carries no `source`. The Chat SDK emits no subscription events, so the kit derives it in ThreadSubscriptionService.applyIncomingReply() and publishes locally. Localization: replaced the six thread-subscription keys that do not exist (thread_mute, thread_unmute, thread_followed_toast, thread_unfollowed_toast, message_list_option_follow_thread, message_list_option_unfollow_thread) with the four thread_subscription_* keys that do, and corrected the three pin/save limit strings, which the same page already quoted correctly 200 lines earlier. Labels: both surfaces use thread_subscription_subscribe / _unsubscribe. The docs built a design rationale on a state-labelled vs action-labelled contrast that is the inverse of what shipped — option-builders.ts:231 says the wording is shared deliberately. SDK version: 4.1.14-beta-1 was never published, and released 4.1.14 contains no subscribeToThread, isPinMessageEnabled or isSystemPinned. All of it lands in 4.2.0. Both guides now state 4.2.0; the pin/save guide previously stated none. Also: messageOptionClick forwards sendMessagePrivately as well as Translate and Report; seven CSS-variable defaults corrected (pinned-messages width is 100%, not the 400px the CSS deliberately abandoned); the conversation pin cap is read via CometChat.getPinnedConversationsLimit(); added the pinned_messages_menu key; and fixed a pre-existing Search Messages link pointing at threaded-messages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKWgNsSpeC73kw9ogmcZRf
…ts own guide In the Accordion's "Guides" row, [Search Messages] linked to /ui-kit/angular/guides/threaded-messages. The earlier fix caught the occurrence in the Guide Directory table but not this one. It does not 404 — the target page exists — so link checkers stay quiet while readers who click Search Messages land on Threaded Messages instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQS41iqMxbd5XnY6wW34pt
Same class as the Search Messages link fixed in cedbc9e — the label and the description describe one guide, the address points at another: Custom Message Types -> guides-overview (x2, the index linking to itself) URL Formatter -> shortcut-formatter Rich Text Formatting -> custom-text-formatter All three intended pages already exist and are in docs.json. Rich Text Formatting is the costliest: a 642-line guide on the built-in editor that nothing in the guides index pointed at, so readers looking to configure the editor landed on the custom-regex-formatter guide instead. None of these 404, so link checkers stay silent. Catching them needs a label-vs-target comparison, not a reachability check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQS41iqMxbd5XnY6wW34pt
Documents the Angular UI Kit work in uikit-angular#559 — thread follow/unfollow, pin and save for messages, and conversation pinning.
New pages
components/cometchat-pinned-messagescomponents/cometchat-saved-messagesguides/thread-subscriptionguides/pin-and-save-messagesUpdated
events.mdx(both event classes, both tiers, the merged observables) ·customization/global-config.mdx(enableThreadSubscription,enablePinMessage,enableSaveMessage) ·core-features.mdx(the app settings that gate it) · message-list, message-bubble, message-header, conversations, conversation-item, thread-header ·guides/threaded-messages,guides/custom-text-formatter, message-composer.Review fixes —
90c48b7fThe review on this PR found that several passages had been written against the design of the feature rather than the branch, and contradicted both the code and other pages inside this same PR. All ten findings were real. Every one is now fixed, verified against
cometchat-team/uikit-angular@ENG-37617-thread-subscription-follow-unfollow, the shippeden-us/translation.json, the component CSS, and the npm registry.Permissions — the headline. The PR stated the pin/unpin rule three different ways: an allow-list on the pinned panel ("only the owner, admins, and moderators see Unpin"), a deny-list in the guide and the message list ("a known participant is denied"). The implementation has neither.
cometchat-pinned-messages.component.ts:615gates on!hideUnpinMessageOption && !isSystemPin()and nothing else; the onlyisParticipantcheck in the message-list option path gates Delete. All three passages now say what the code does — no client-side role gate, the server is the sole authority and refuses withERR_ACTION_NOT_ALLOWED, and the one thing withheld on the client is Unpin on a system pin. The<Note>promising that a participant would not see Unpin is gone; it described the exact scenario an integrator would have written handling for.Code that would not have worked if copied.
event.state→event.subscribedin both samples.IThreadSubscriptionChangeis{ parentMessageId, subscribed }, which the events table twenty lines below already said.sourcedestructure and the paragraph documenting it as an API contract.IPinSaveChangedis{ message }— the two-tier design replaced a source discriminator. Replaced with the tiers themselves (server truth vs this client's optimism) and a pointer at the mergedpinned$/unpinned$/saved$/unsaved$.Auto-subscribe does not arrive on an SDK thread listener and carries no
source. The Chat SDK emits no subscription events at all, so the kit derives it inThreadSubscriptionService.applyIncomingReply()and publishes locally.Localization. Six of the eight thread-subscription keys did not exist —
thread_mute,thread_unmute,thread_followed_toast,thread_unfollowed_toast,message_list_option_follow_thread,message_list_option_unfollow_thread— replaced with the four realthread_subscription_*keys. The three pin/save limit strings were wrong and are corrected; the same page already quoted the right copy 200 lines earlier.Labels. Both surfaces use
thread_subscription_subscribe/_unsubscribe→ "Subscribe to thread" / "Unsubscribe from thread". The docs had built a design rationale on a state-labelled vs action-labelled contrast that is the inverse of what shipped —option-builders.ts:231says the wording is shared deliberately.SDK version.
4.1.14-beta-1was never published, and released4.1.14contains nosubscribeToThread,isPinMessageEnabledorisSystemPinned— all of it lands in4.2.0. Both guides now state 4.2.0; the pin/save guide previously stated no version at all.Also:
messageOptionClickforwardssendMessagePrivatelyas well as Translate and Report; seven CSS-variable defaults corrected (the pinned panel's width is100%, not the400pxthe CSS deliberately abandoned); the conversation pin cap is read viaCometChat.getPinnedConversationsLimit(); added thepinned_messages_menukey; and fixed a pre-existing Search Messages link that pointed atthreaded-messages.Written against the branch
The first pass caught several things a read of the design would have got wrong, and those still hold:
ThreadSubscriptionServicetakes the message, not a parent id, and returns a booleanccMessagePinChanged/ccMessageSaveChanged) and the mergedpinned$/unpinned$/saved$/unsaved$are what a surface should subscribe to, not the raw subjectsConversation pins carry two reaches and the docs separate them: a personal pin is private and multi-device, an admin pin is app-wide (
pinnedBy === "app_system"), distinguished byisSystemPinned().Where that discipline slipped — the permissions rule, the two payload shapes, the label pair, the localization keys — the review caught it and
90c48b7ffixes it.Storybook
Both new component pages carry a live preview, matching the pattern the neighbouring pages already use. All 44 story ids referenced across
ui-kit/angularresolve againststorybook.cometchat.io/angular.Note on the branch
Branched from current
mainrather than continuing #454, which had fallen 53 commits behind. Content is identical; onlydocs.jsondiffers, rebuilt on main's newer navigation. #454 can be closed in favour of this.Verified
docs.jsonparses, and everyui-kit/angularnav entry resolves to a file — unchanged by the review fixes, which touch prose only90c48b7f: every localization row on the four affected pages matchesen-us/translation.jsonexactly; a full CSS-variable audit of both new component pages found no remaining mismatches; MDX fences and<Note>/<Warning>/<Accordion>tags balanced across all seven edited files; both touched links resolve (guides/search-messages.mdxexists,#system-pinsis a heading on the same page)event.state,source,thread_mute,4.1.14-beta-1or old-label strings anywhere underui-kit/angular🤖 Generated with Claude Code
https://claude.ai/code/session_01GKWgNsSpeC73kw9ogmcZRf