Skip to content

docs(ui-kit/angular): thread subscription, pin & save messages and conversations - #495

Merged
ketanyekale merged 4 commits into
mainfrom
docs/angular-thread-pin-save
Sep 9, 2026
Merged

ketanyekale merged 4 commits into
mainfrom
docs/angular-thread-pin-save

Conversation

@aanshisingh-cometchat

@aanshisingh-cometchat aanshisingh-cometchat commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Documents the Angular UI Kit work in uikit-angular#559 — thread follow/unfollow, pin and save for messages, and conversation pinning.

New pages

Page
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 (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 — 90c48b7f

The 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 shipped en-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: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. 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.stateevent.subscribed in both samples. IThreadSubscriptionChange is { parentMessageId, subscribed }, which the events table twenty lines below already said.
  • Dropped the source destructure and the paragraph documenting it as an API contract. IPinSaveChanged is { 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 merged pinned$ / 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 in ThreadSubscriptionService.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 real thread_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: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 no version at all.

Also: messageOptionClick forwards sendMessagePrivately as well as Translate and Report; seven CSS-variable defaults corrected (the pinned panel's 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 that pointed at threaded-messages.

Written against the branch

The first pass caught several things a read of the design would have got wrong, and those still hold:

  • ThreadSubscriptionService takes the message, not a parent id, and returns a boolean
  • the toggle is leading-edge: the write leaves on the first tap and anything within 400 ms is swallowed — not collapsed into a trailing call
  • the optimistic tier (ccMessagePinChanged / ccMessageSaveChanged) and the merged pinned$ / unpinned$ / saved$ / unsaved$ are what a surface should subscribe to, not the raw subjects

Conversation 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 by isSystemPinned().

Where that discipline slipped — the permissions rule, the two payload shapes, the label pair, the localization keys — the review caught it and 90c48b7f fixes 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/angular resolve against storybook.cometchat.io/angular.

Note on the branch

Branched from current main rather than continuing #454, which had fallen 53 commits behind. Content is identical; only docs.json differs, rebuilt on main's newer navigation. #454 can be closed in favour of this.

Verified

  • docs.json parses, and every ui-kit/angular nav entry resolves to a file — unchanged by the review fixes, which touch prose only
  • the site builds and serves locally, both new pages included
  • after 90c48b7f: every localization row on the four affected pages matches en-us/translation.json exactly; 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.mdx exists, #system-pins is a heading on the same page)
  • no residual event.state, source, thread_mute, 4.1.14-beta-1 or old-label strings anywhere under ui-kit/angular

🤖 Generated with Claude Code

https://claude.ai/code/session_01GKWgNsSpeC73kw9ogmcZRf

…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
@mintlify

mintlify Bot commented Sep 7, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
cometchat 🟢 Ready View Preview Sep 7, 2026, 4:41 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@raj-dubey1

Copy link
Copy Markdown
Contributor

Docs PR review — structural checks clean, content has real drift

Verdict: 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 undefined if a reader copied them.

I verified every API claim against cometchat-team/uikit-angular@ENG-37617-thread-subscription-follow-unfollow and against the published @cometchat/chat-sdk-javascript typings, not against the PR description.


✅ What passed (actually checked, not assumed)

Check Result
Redirect coverage 0 needed — 0 files removed, 0 renamed. Purely additive; leaving redirects untouched is correct
Chained 404s 0 — no existing redirect target is deleted
Navigation integrity 0 dangling pages refs in docs.json. Build is safe
Orphans 1, /ui-kit/angular/2.0/javascript-2-0-overview — pre-existing on main, not this PR
In-content link rot 0 broken internal links across 107 in-scope files
Heading anchors 0 broken — checked all #-anchored links in the 18 changed pages against real headings (#permissions, #enabling-the-feature, #toolbar-trailing-view, #enablethreadsubscription, etc. all resolve)
Storybook 86/86 story refs resolve against storybook.cometchat.io/angular/index.json, including both new components — the stories are already deployed, so the iframes will render
Placeholders / images 0 TODO/FIXME/lorem; no image refs to break
Nav registration New pages correctly grouped ("Pinned & Saved"), overview pages updated

Also verified correct against source: the whole events.mdx addition (both tiers, all 9 subjects, all 6 merged observables, all 6 typed helpers); the PinSaveService member table (all 10 exist with those signatures); the CometChatSavedMessages property/event tables (exact match); toolbarTrailingView and its template context; showPinnedMessagesOption / pinnedMessagesClick; the "overflow menu only appears when more than one option is set" note (shouldShowOverflowMenu is filter(Boolean).length > 1 — correct); the parentMessage / deprecated parentMessageId migration; 21/21 pin-save localization keys; conversation.isSystemPinned() (confirmed in the 4.2.0 typings); and the pinned panel's fetch behaviour (limit 100, 30-row local window).


P0 — must fix before merge

1. Three mutually contradictory permission stories for Pin/Unpin, and none matches the code

This is the headline. The PR states the same rule three different ways:

  • components/cometchat-pinned-messages.mdx:18,242,247allow-list: "only the owner, admins, and moderators see Unpin", "Ownership is read from the group's owner UID rather than the member scope", and a <Note> asserting "Role gating always wins over the input. Passing [hideUnpinMessageOption]="false" cannot grant a participant an unpin…"
  • guides/pin-and-save-messages.mdx:105,110 and components/cometchat-message-list.mdx:435deny-list: "a known participant is denied and everyone else is offered the option", with a <Note> explaining why an allow-list was rejected.

The implementation has neither. cometchat-pinned-messages.component.ts:615:

if (!this.hideUnpinMessageOption && !this.isSystemPin(message)) {

That is the entire gate — no scope check anywhere in the component (grep -i moderator returns nothing). The component's own TSDoc at :130-137 says so explicitly: "There is no client-side role gate, so this input is the only thing that withholds Unpin — apart from a system pin… A member without the permission still sees the option; the server refuses the call and the optimistic flip reverts."

Same for the message list — cometchat-message-list.option-builders.ts:122: "Neither action is scope-gated on the client: Pin is offered to every member and the server decides, refusing with ERR_ACTION_NOT_ALLOWED." (The isParticipant check at :42 is for Delete, not pin.)

The <Note> at cometchat-pinned-messages.mdx:247 is the most damaging line: it tells an integrator that a participant will not see Unpin, when in fact they will and the call will fail server-side — the exact scenario they'd write handling for. All three passages need to be replaced with: no client-side role gate; the only client-side withholding is a system pin; the server is the sole authority and refuses with ERR_ACTION_NOT_ALLOWED.

2. event.state — the field does not exist (2 code examples)

guides/thread-subscription.mdx:105 and components/cometchat-thread-header.mdx:192:

console.log(event.parentMessageId, event.state);

IThreadSubscriptionChange (cometchat-thread-header.component.ts:42) is { parentMessageId: number; subscribed: boolean }. There is no state. This contradicts the Events table 20 lines below it on the same page (cometchat-thread-header.mdx:214, which correctly says { parentMessageId: number; subscribed: boolean }), contradicts events.mdx, and contradicts this PR's own description ("there is no state or source field"). → event.subscribed.

3. source on CometChatPinSaveEvents payloads — the field does not exist

guides/pin-and-save-messages.mdx:261,264 destructures it, and :287 documents it as an API contract:

The source field is an open string: "optimistic" for a local flip not yet acked, "revert" when a failure put it back, and "sdk" for a confirmed write or a realtime frame. An unrecognised value must never break a subscriber.

IPinSaveChanged is { message: CometChat.BaseMessage } — nothing else. publishTruth() sends const payload = { message }. The two-tier design replaced a source discriminator; that's the whole point of pinned$/unpinned$. This paragraph and the destructure need to go, and events.mdx (which gets it right) is the model.

4. Auto-subscribe mechanism described wrong

guides/thread-subscription.mdx:251:

That arrives through the SDK's thread listener and is re-emitted on ccThreadSubscriptionChanged with source: "sdk"

Both halves are wrong, and it contradicts the same page 20 lines earlier ("The Chat SDK emits no subscription events of its own"). CometChatThreadEvents.ts:29"the Chat SDK no longer emits subscription events, so everything on this subject originates in the kit." The auto-subscribe is derived client-side in ThreadSubscriptionService.applyIncomingReply(), which inspects an incoming thread reply for an @mention of the logged-in user (Case 3) or the user's own reply (Case 4) and calls mirrorSubscribed() — deliberately without re-issuing subscribeToThread.

5. Thread-subscription localization table — 6 of 8 keys do not exist

guides/thread-subscription.mdx:267-274. Checked against resources/en-us/translation.json:

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:231 says the conversation pin cap comes "with the real number from the rejection", while components/cometchat-conversations.mdx says it's read via CometChat.getPinnedConversationsLimit(). The latter matches cometchat-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.mdx diff 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
@ketanyekale
ketanyekale merged commit f98af7c into main Sep 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

6 participants