feat: controlled comments API + derive tracked changes from PM (collab-ready)#255
Merged
jedrazb merged 3 commits intofeat/collaboration-examplefrom Apr 13, 2026
Merged
Conversation
Unblocks Yjs collaboration for comments and confirms tracked changes already sync via ySyncPlugin. **Comments — additive controlled mode** (new optional props): - `comments: Comment[]` — when passed, the editor reads thread metadata from the prop instead of internal state - `onCommentsChange: (comments: Comment[]) => void` — fires on every mutation (add, reply, resolve, unresolve, delete, orphan cleanup) When omitted, the editor falls back to internal `useState<Comment[]>([])`. Existing consumers see no behavior change. Implemented via a small `useControllableComments` hook (Radix-style controlled/uncontrolled pattern). All existing `setComments` call sites are unchanged — they route through the hook's setter. **Tracked changes — eliminate the redundant React state**: - `useTrackedChanges(state)` derives `TrackedChangeEntry[]` from the current PM state via memoized pure-function extraction - DocxEditor mirrors the latest PM state on every doc-changing transaction (including remote ones from ySyncPlugin) - 300ms debounce on extraction is gone — sidebar updates immediately - Comment-to-tracked-change threading moved to its own effect that depends on `[trackedChanges, pmState]` Tracked changes already worked over Yjs because metadata lives in the `insertion`/`deletion` mark attrs (synced with the PM tree). The React state was a redundant derived view. **Demo (`examples/collaboration`)**: - `useCollaboration` now exposes `comments` + `setComments`, mirrored against a `Y.Array<Comment>` on the same `Y.Doc` - `App.tsx` passes them as the controlled props - README documents the four-piece architecture and the comments replace-all caveat (production should use `Y.Map<id, Comment>`) **Docs**: `docs/PROPS.md` adds the two new props + a "Controlled Comments" section with a Yjs Y.Array bridge example. Stacked on PR #254 (the demo creation) — base will retarget to main once that lands. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Code review fixes for PR #255: - **Eliminate duplicate doc walk** (perf review): comment→revision overlap map is now built in the same `doc.descendants` pass as tracked-change extraction. Halves per-keystroke walk cost on large docs with tracked changes. `useTrackedChanges` returns `{ entries, commentToRevision }`. - **Drop random number from display name** (UX review): `Ada 42` → `Ada`, fixes `A4` initials in AvatarStack. - **Strengthen demo README caveats** (UX review): document the data-loss risk of the naive Y.Array replace-all sync, link to remote-cursor issue (#256) and comment-id-collision issue (#257), call out tracked-change accept/reject race. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address the two items deferred from the first review pass:
- **Inline `useControllableComments`** (simplifier): the hook had one
call site and ~50 lines of plumbing for a pattern that's ~12 lines
inline. Folded into DocxEditor right next to the existing
`commentsRef`/`onCommentDeleteRef` block. Hook file deleted.
- **Replace 200ms initial-load setTimeout** (code review): the timer
existed because there was no deterministic signal that the document
had been loaded into PM. Replaced with two synchronous paths:
- `PagedEditor.onReady` callback (line ~4019) now also seeds `pmState`
once on mount, before any external consumer's `onEditorViewReady`
fires.
- The `[state.isLoading, history.state]` effect runs synchronously
(no timer) — by React's child-first effect ordering, `view.state`
already reflects the new doc by the time the effect runs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
jedrazb
added a commit
that referenced
this pull request
Apr 13, 2026
* feat(examples): add realtime collaboration example with Yjs + y-webrtc Demonstrates multi-user collaborative editing on top of the new externalContent prop using y-prosemirror over WebRTC. No backend required — peers connect via the public Yjs signaling servers. - src/useCollaboration.ts: Y.Doc, WebrtcProvider, awareness, plugins - src/AvatarStack.tsx: Google-Docs-style overlapping avatars in title bar - src/identity.ts: per-tab user identity + room from URL hash Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(examples/collaboration): dev script + dedupe + room status placement - run vite from monorepo root so Tailwind's relative content paths resolve - dedupe react/react-dom (workspace + example were loading separate copies, causing "Cannot read properties of null (reading 'useMemo')") - move room status next to Share link button on the right; logo area is just the GitHub badge Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: controlled comments API + derive tracked changes from PM (collab-ready) (#255) * feat: controlled comments API + derive tracked changes from PM Unblocks Yjs collaboration for comments and confirms tracked changes already sync via ySyncPlugin. **Comments — additive controlled mode** (new optional props): - `comments: Comment[]` — when passed, the editor reads thread metadata from the prop instead of internal state - `onCommentsChange: (comments: Comment[]) => void` — fires on every mutation (add, reply, resolve, unresolve, delete, orphan cleanup) When omitted, the editor falls back to internal `useState<Comment[]>([])`. Existing consumers see no behavior change. Implemented via a small `useControllableComments` hook (Radix-style controlled/uncontrolled pattern). All existing `setComments` call sites are unchanged — they route through the hook's setter. **Tracked changes — eliminate the redundant React state**: - `useTrackedChanges(state)` derives `TrackedChangeEntry[]` from the current PM state via memoized pure-function extraction - DocxEditor mirrors the latest PM state on every doc-changing transaction (including remote ones from ySyncPlugin) - 300ms debounce on extraction is gone — sidebar updates immediately - Comment-to-tracked-change threading moved to its own effect that depends on `[trackedChanges, pmState]` Tracked changes already worked over Yjs because metadata lives in the `insertion`/`deletion` mark attrs (synced with the PM tree). The React state was a redundant derived view. **Demo (`examples/collaboration`)**: - `useCollaboration` now exposes `comments` + `setComments`, mirrored against a `Y.Array<Comment>` on the same `Y.Doc` - `App.tsx` passes them as the controlled props - README documents the four-piece architecture and the comments replace-all caveat (production should use `Y.Map<id, Comment>`) **Docs**: `docs/PROPS.md` adds the two new props + a "Controlled Comments" section with a Yjs Y.Array bridge example. Stacked on PR #254 (the demo creation) — base will retarget to main once that lands. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * review fixes: single-walk derivation + demo polish Code review fixes for PR #255: - **Eliminate duplicate doc walk** (perf review): comment→revision overlap map is now built in the same `doc.descendants` pass as tracked-change extraction. Halves per-keystroke walk cost on large docs with tracked changes. `useTrackedChanges` returns `{ entries, commentToRevision }`. - **Drop random number from display name** (UX review): `Ada 42` → `Ada`, fixes `A4` initials in AvatarStack. - **Strengthen demo README caveats** (UX review): document the data-loss risk of the naive Y.Array replace-all sync, link to remote-cursor issue (#256) and comment-id-collision issue (#257), call out tracked-change accept/reject race. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * review fixes round 2: inline controlled comments + drop 200ms timer Address the two items deferred from the first review pass: - **Inline `useControllableComments`** (simplifier): the hook had one call site and ~50 lines of plumbing for a pattern that's ~12 lines inline. Folded into DocxEditor right next to the existing `commentsRef`/`onCommentDeleteRef` block. Hook file deleted. - **Replace 200ms initial-load setTimeout** (code review): the timer existed because there was no deterministic signal that the document had been loaded into PM. Replaced with two synchronous paths: - `PagedEditor.onReady` callback (line ~4019) now also seeds `pmState` once on mount, before any external consumer's `onEditorViewReady` fires. - The `[state.isLoading, history.state]` effect runs synchronously (no timer) — by React's child-first effect ordering, `view.state` already reflects the new doc by the time the effect runs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two changes that together make comments and tracked changes work over Yjs:
Comments — additive controlled mode. New optional `comments` + `onCommentsChange` props on `DocxEditor`. When passed, the editor reads thread metadata (text, author, replies, resolved status) from the prop and emits every change through the callback. Pair with a `Y.Array` (or any other shared collection) to sync threads across collaborators. Existing consumers see no behavior change — fallback to internal `useState` is identical to today.
Tracked changes — eliminate the redundant React state. `useState<TrackedChangeEntry[]>([])` was a debounced, derived view of data already in PM mark attrs (`insertion`/`deletion` carry `revisionId`/`author`/`date`). Replaced by a `useTrackedChanges(state)` hook that derives synchronously on every doc-changing transaction. Tracked changes already collab-sync through `ySyncPlugin` because the metadata rides along with the synced PM tree — this PR just removes the redundant copy and confirms the path.
Demo: the existing `examples/collaboration` is updated to wire comments through a `Y.Array` on the same `Y.Doc`. Open the demo in two browser windows (Share link → paste in second tab) to verify both flows end-to-end.
What changed
API surface
```tsx
<DocxEditor
document={createEmptyDocument()}
externalContent
externalPlugins={[ySync, yCursor, yUndo]}
comments={comments} // NEW: controlled
onCommentsChange={setComments} // NEW: fires on every change
// ...
/>
```
The granular `onCommentAdd`/`onCommentResolve`/`onCommentDelete`/`onCommentReply` callbacks still fire alongside `onCommentsChange` — parents that just want notifications don't need to switch.
Why tracked changes don't need a controlled API
Originally I assumed tracked-changes metadata lived in React state too. Code reading proved otherwise: it's already in PM mark attrs, parsed from `<w:ins>`/`<w:del>` and serialized back via `fromProseDoc.ts`. The React state was a debounced derived view used by the sidebar. That copy is now gone — the sidebar derives directly from PM via the new hook, and remote ySync updates flow through automatically.
This matches superdoc's split: comments use an external entity store (their `comment-entity-store.ts` + `Y.Map` collab adapter), tracked changes live entirely in PM marks.
Review pass (4 reviewers, applied fixes inline)
Test plan
Risks
Follow-ups filed