Skip to content

feat(channels/documents): Stanalone channel thread + Full reactive thread in document cards#2734

Merged
synoet merged 13 commits into
mainfrom
macro-czfKSYdQPm7FSzH3JSDsz-render-thread-in-channel-message-mention-card
Apr 22, 2026
Merged

feat(channels/documents): Stanalone channel thread + Full reactive thread in document cards#2734
synoet merged 13 commits into
mainfrom
macro-czfKSYdQPm7FSzH3JSDsz-render-thread-in-channel-message-mention-card

Conversation

@synoet
Copy link
Copy Markdown
Contributor

@synoet synoet commented Apr 21, 2026

  • Adds support for rendering standalone channel threads anywhere with just a channel and message id.
  • Adds a ReadonlyStandaloneThread to the channel message card preview.
  • Slight adjustments to the preview component, as channels require the same rendering stability as pdf's on updates.
CleanShot.2026-04-22.at.11.06.44.mp4

@macro-application
Copy link
Copy Markdown

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

Warning

Rate limit exceeded

@synoet has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 42 minutes and 19 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 42 minutes and 19 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 585feb40-14ad-402e-9e91-5122f73a9d77

📥 Commits

Reviewing files that changed from the base of the PR and between c33f60e and 439ea29.

📒 Files selected for processing (1)
  • js/app/packages/block-channel/utils/link.ts
📝 Walkthrough

Walkthrough

Adds a StandaloneThread component system with editable/readonly variants, introduces navigateToChannelMessage to open/activate channel splits and navigate to a message/thread, implements by-IDs channel message queries and cache helpers, and updates notification navigation to use the new navigation utility.

Changes

Cohort / File(s) Summary
StandaloneThread components
js/app/packages/channel/StandaloneThread/Root.tsx, js/app/packages/channel/StandaloneThread/ParentMessage.tsx, js/app/packages/channel/StandaloneThread/Replies.tsx, js/app/packages/channel/StandaloneThread/ReadonlyThread.tsx, js/app/packages/channel/StandaloneThread/EditableThread.tsx, js/app/packages/channel/StandaloneThread/context.ts, js/app/packages/channel/StandaloneThread/StandaloneThread.ts, js/app/packages/channel/StandaloneThread/index.ts
New SolidJS thread component set: provider/root that fetches parent and replies, context hooks, ParentMessage and Replies renderers, and Readonly/Editable wrappers with reply input and message actions.
Channel navigation utilities & integrations
js/app/packages/block-channel/utils/link.ts, js/app/packages/notifications/notification-navigation.ts
Added exported async navigateToChannelMessage(...) to open/activate channel splits (uses optional injected SplitManager) and navigate to a message; notification navigation now delegates channel message navigation to this helper.
Channel message queries & reconciliation
js/app/packages/queries/channel/channel-messages.ts, js/app/packages/queries/channel/keys.ts, js/app/packages/queries/channel/reconcile.ts
Added useChannelMessagesByIdsQuery, messages-by-ids query key generator, cache-set/invalidation helpers, and utilities to update top-level reactions/attachments/state in by-IDs caches; reconciler now syncs these by-ids caches alongside existing paginated caches.
API client & rich-text preview integration
js/app/packages/service-clients/service-comms/client.ts, js/app/packages/core/component/LexicalMarkdown/component/decorator/ChannelMessageThreadCard.tsx, js/app/packages/core/component/LexicalMarkdown/component/decorator/DocumentCard.tsx
Added postChannelMessages client method for filtered message fetches; added ChannelMessageThreadCard component that renders ReadonlyThread and invokes navigation helper; DocumentCard updated to render channel thread previews and adjust selection/click behavior.

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title exceeds the 72-character limit at 91 characters and does not strictly follow conventional commits format. Shorten the title to under 72 characters, e.g., 'feat(channels): Add standalone thread rendering' or 'feat(documents): Render channel threads in cards'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description is clearly related to the changeset, explaining the addition of standalone channel thread rendering and its integration into document card previews.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 21, 2026

@synoet synoet marked this pull request as ready for review April 22, 2026 15:21
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
js/app/packages/core/component/LexicalMarkdown/component/decorator/DocumentCard.tsx (1)

427-459: ⚠️ Potential issue | 🟠 Major

Exclude channel message cards from block-preview sizing.

The creation path correctly skips createBlockInstance when channelMessageId() exists, but the render path still uses isPreviewable() for resize classes, fixed height, and the scroll wrapper. If a channel is previewable, the thread card gets block-preview layout behavior accidentally.

Suggested change
+  const isResizableBlockPreview = () => !channelMessageId() && isPreviewable();
+
   return (
     <div
@@
       classList={{
         'bg-active outline-edge outline-4':
           isSelectedAsNode() && !channelMessageId(),
-        'resize-y shrink-0 min-h-[100px]': isPreviewable(),
+        'resize-y shrink-0 min-h-[100px]': isResizableBlockPreview(),
       }}
       style={{
-        height: isPreviewable() ? previewBoxHeight : 'auto',
+        height: isResizableBlockPreview() ? previewBoxHeight : 'auto',
       }}
@@
                 <Show
-                  when={isPreviewable()}
+                  when={isResizableBlockPreview()}
                   fallback={<Dynamic component={previewComponent()} />}
                 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@js/app/packages/core/component/LexicalMarkdown/component/decorator/DocumentCard.tsx`
around lines 427 - 459, The render still applies block-preview sizing to channel
message cards; update all preview-related checks to also ensure the card is not
a channel message by using !channelMessageId() in addition to isPreviewable().
Specifically, change the conditional that adds 'resize-y shrink-0 min-h-[100px]'
(currently isPreviewable()) to (!channelMessageId() && isPreviewable()), change
the inline style height (currently height: isPreviewable() ? previewBoxHeight :
'auto') to use (!channelMessageId() && isPreviewable()), and wrap the preview
Show/Dynamic scroll container so it only renders when (!channelMessageId() &&
isPreviewable()) to prevent channel threads from getting preview layout or
scroll behavior; leave clickCardHandler early return unchanged.
js/app/packages/queries/channel/reconcile.ts (1)

166-188: ⚠️ Potential issue | 🟠 Major

Mirror top-level delete and restore into the by-IDs cache.

removeMessageFromTargetCaches and restoreMessageInTargetCaches still only touch setChannelMessagesData. A standalone thread loaded via useChannelMessagesByIdsQuery can keep rendering a deleted parent, and rollback has no by-IDs snapshot to restore. Add by-IDs remove/restore helpers and capture enough top-level snapshot data for that cache too.

Also applies to: 222-255

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@js/app/packages/queries/channel/reconcile.ts` around lines 166 - 188,
removeMessageFromTargetCaches and restoreMessageInTargetCaches only update the
channel messages list (setChannelMessagesData) and neglect the by-IDs cache used
by useChannelMessagesByIdsQuery, so standalone threads can still show deleted
parents and have no snapshot to restore; update both functions to also mutate
the by-IDs cache via queryClient.setQueryData for the channel-by-ids key
(mirroring what you do for thread replies with getThreadRepliesQueryKey),
using/adding helper routines like removeMessageFromByIds and
restoreMessageInByIds to remove/restore entries and ensure you capture any
necessary top-level snapshot data (e.g., message content and parent/threadId)
before deletion so the by-IDs cache can be correctly rolled back.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@js/app/packages/block-channel/utils/link.ts`:
- Around line 39-49: The helper currently always calls globalSplitManager() and
ignores the SplitManager/newSplit flags from openChannelNotification; change it
to accept/use a passed-in SplitManager and newSplit flag (use the passed
splitManager variable if provided, fallback to globalSplitManager only as a last
resort), avoid returning early when a different manager is intended, and when
opening a new split call splitManager.openWithSplit with the option that conveys
the new-split intent (e.g., preferNewSplit or newSplit) instead of hard-coding {
activate: true, referredFrom: null}; update references to splitManager,
globalSplitManager(), openWithSplit, openChannelNotification and newSplit
accordingly so notification navigation can pass { splitManager: layoutManager,
newSplit } through.

In `@js/app/packages/channel/StandaloneThread/ParentMessage.tsx`:
- Around line 17-25: The message root is not keyboard-accessible when clickable;
update ParentMessage.tsx so that when props.onClickMessage is provided the
Message.Root receives keyboard semantics: make it focusable (e.g., tabIndex=0),
add an appropriate role (e.g., role="button" or other semantic role matching the
action), and add an onKeyDown handler that triggers
props.onClickMessage(parentMsg().id, event) when Enter or Space is pressed (and
prevent default for Space). Keep the existing onClick behavior and ensure aria
attributes remain unchanged unless needed for state.

In `@js/app/packages/channel/StandaloneThread/Replies.tsx`:
- Around line 63-111: The ActionMenu is being rendered whenever
props.getMessageActions exists even if it returns undefined for a specific
reply; call props.getMessageActions with the current reply (e.g., const actions
= props.getMessageActions?.(reply)) and only render <Message.ActionMenu /> when
that result is truthy (or has items) — replace the Show check around
Message.ActionMenu to use the per-reply actions value instead of just
props.getMessageActions.

In `@js/app/packages/channel/StandaloneThread/Root.tsx`:
- Around line 16-21: The Root component is using DebugSuspense which emits logs
in production; replace DebugSuspense with React's standard Suspense (or remove
the wrapper entirely if no fallback is needed) so production console noise is
avoided—locate the Root function and change the wrapper around <RootInner
{...props} /> from DebugSuspense to Suspense (importing Suspense if missing) or
remove the wrapper when safe.

In `@js/app/packages/queries/channel/channel-messages.ts`:
- Around line 113-126: The query options factory reads the accessors channelId()
and messageIds() multiple times causing potential mismatch between queryKey and
queryFn; fix by snapshotting their values once at the top (e.g., const
channelIdVal = channelId(); const messageIdsVal = messageIds()) and then use
those captured values when building channelKeys.messagesByIds(...).queryKey, in
the queryFn call to commsServiceClient.postChannelMessages, and for
enabled/staleTime checks so the key and function always use the same inputs.

---

Outside diff comments:
In
`@js/app/packages/core/component/LexicalMarkdown/component/decorator/DocumentCard.tsx`:
- Around line 427-459: The render still applies block-preview sizing to channel
message cards; update all preview-related checks to also ensure the card is not
a channel message by using !channelMessageId() in addition to isPreviewable().
Specifically, change the conditional that adds 'resize-y shrink-0 min-h-[100px]'
(currently isPreviewable()) to (!channelMessageId() && isPreviewable()), change
the inline style height (currently height: isPreviewable() ? previewBoxHeight :
'auto') to use (!channelMessageId() && isPreviewable()), and wrap the preview
Show/Dynamic scroll container so it only renders when (!channelMessageId() &&
isPreviewable()) to prevent channel threads from getting preview layout or
scroll behavior; leave clickCardHandler early return unchanged.

In `@js/app/packages/queries/channel/reconcile.ts`:
- Around line 166-188: removeMessageFromTargetCaches and
restoreMessageInTargetCaches only update the channel messages list
(setChannelMessagesData) and neglect the by-IDs cache used by
useChannelMessagesByIdsQuery, so standalone threads can still show deleted
parents and have no snapshot to restore; update both functions to also mutate
the by-IDs cache via queryClient.setQueryData for the channel-by-ids key
(mirroring what you do for thread replies with getThreadRepliesQueryKey),
using/adding helper routines like removeMessageFromByIds and
restoreMessageInByIds to remove/restore entries and ensure you capture any
necessary top-level snapshot data (e.g., message content and parent/threadId)
before deletion so the by-IDs cache can be correctly rolled back.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8aabd4a1-5467-42e9-9812-ebcaae4d0539

📥 Commits

Reviewing files that changed from the base of the PR and between 6075176 and 02c5cd5.

📒 Files selected for processing (16)
  • js/app/packages/block-channel/utils/link.ts
  • js/app/packages/channel/StandaloneThread/EditableThread.tsx
  • js/app/packages/channel/StandaloneThread/ParentMessage.tsx
  • js/app/packages/channel/StandaloneThread/ReadonlyThread.tsx
  • js/app/packages/channel/StandaloneThread/Replies.tsx
  • js/app/packages/channel/StandaloneThread/Root.tsx
  • js/app/packages/channel/StandaloneThread/StandaloneThread.ts
  • js/app/packages/channel/StandaloneThread/context.ts
  • js/app/packages/channel/StandaloneThread/index.ts
  • js/app/packages/core/component/LexicalMarkdown/component/decorator/ChannelMessageThreadCard.tsx
  • js/app/packages/core/component/LexicalMarkdown/component/decorator/DocumentCard.tsx
  • js/app/packages/notifications/notification-navigation.ts
  • js/app/packages/queries/channel/channel-messages.ts
  • js/app/packages/queries/channel/keys.ts
  • js/app/packages/queries/channel/reconcile.ts
  • js/app/packages/service-clients/service-comms/client.ts

Comment thread js/app/packages/block-channel/utils/link.ts Outdated
Comment thread js/app/packages/channel/StandaloneThread/ParentMessage.tsx
Comment thread js/app/packages/channel/StandaloneThread/Replies.tsx
Comment thread js/app/packages/channel/StandaloneThread/Root.tsx
Comment thread js/app/packages/queries/channel/channel-messages.ts Outdated
@synoet synoet requested review from gbirman and sedson April 22, 2026 15:55
@gbirman
Copy link
Copy Markdown
Contributor

gbirman commented Apr 22, 2026

@synoet fix reply navigation on click

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@js/app/packages/block-channel/utils/link.ts`:
- Around line 61-62: The call to handle?.goToLocationFromParams(params) does not
await the async navigation so navigateToChannelMessage may resolve early and
swallow rejections; change it to check for a non-null handle (using
orchestrator.getBlockHandle / getBlockHandle result) and await
handle.goToLocationFromParams(params) so the promise is propagated (i.e., if
handle exists, call await handle.goToLocationFromParams(params) inside
navigateToChannelMessage).

In `@js/app/packages/channel/StandaloneThread/Replies.tsx`:
- Around line 28-38: collapsedRepliesCount currently uses server count
(ctx.parent()?.thread.reply_count) while collapsedReplyUsers and
collapsedLatestReplyAt use ctx.replies(), causing inconsistent UI; update the
three helpers (collapsedRepliesCount, collapsedReplyUsers,
collapsedLatestReplyAt) to use the same source: if ctx.replies().length > 0 and
has at least DEFAULT_VISIBLE_REPLY_COUNT entries use ctx.replies() for
count/users/latest, otherwise fall back to server preview values from
ctx.parent()?.thread.preview and ctx.parent()?.thread.latest_reply_at so the
visible participants list and latestReplyAt stay consistent with the shown count
(adjust calls to getCollapsedRepliesCount, getUniqueReplyUserIds,
getThreadLatestReplyAt to accept the chosen source).

In
`@js/app/packages/core/component/LexicalMarkdown/component/decorator/DocumentCard.tsx`:
- Around line 453-461: The fallback currently renders previewComponent()
whenever isPreviewable() is false, which allows non-channel previews to bypass
nesting/block checks; change the fallback so it only renders the cached/custom
preview when it is a channel-thread preview. Concretely, update the outer
Show/fallback logic around previewComponent() and isPreviewable() so the
fallback Dynamic component is only returned when a new predicate (e.g.,
isChannelThreadPreview() or a check on previewComponentMetadata) is true; if
such a predicate does not exist, add a small helper (isChannelThreadPreview) and
use it in the fallback expression so only channel thread previews can render the
non-previewable fallback while other non-previewable previews remain blocked.

In `@js/app/packages/queries/channel/channel-messages.ts`:
- Around line 594-679: The query key currently can vary by message ID order;
update getChannelMessagesByIdsQueryKeyPrefix to accept the messageIds array
(e.g., getChannelMessagesByIdsQueryKeyPrefix(channelId: string, messageIds:
string[])) and compose the key using a sorted copy of messageIds (e.g.,
[...channelKeys.messagesByIds._def, channelId, sortedMessageIds]) so identical
sets produce the same key; then update any callers (like
setChannelMessagesByIdsData and any consumers that build the by-ids key) to pass
messageIds through this helper so keys are normalized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a51ad33e-ada0-4a1c-ad05-1c9072a745fa

📥 Commits

Reviewing files that changed from the base of the PR and between 02c5cd5 and c33f60e.

📒 Files selected for processing (6)
  • js/app/packages/block-channel/utils/link.ts
  • js/app/packages/channel/StandaloneThread/Replies.tsx
  • js/app/packages/core/component/LexicalMarkdown/component/decorator/ChannelMessageThreadCard.tsx
  • js/app/packages/core/component/LexicalMarkdown/component/decorator/DocumentCard.tsx
  • js/app/packages/notifications/notification-navigation.ts
  • js/app/packages/queries/channel/channel-messages.ts

Comment thread js/app/packages/block-channel/utils/link.ts Outdated
Comment thread js/app/packages/channel/StandaloneThread/Replies.tsx
Comment thread js/app/packages/queries/channel/channel-messages.ts
@synoet synoet merged commit 2d4b3a3 into main Apr 22, 2026
23 checks passed
@synoet synoet deleted the macro-czfKSYdQPm7FSzH3JSDsz-render-thread-in-channel-message-mention-card branch April 22, 2026 19:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants