Notes: Support multiple note threads per block#75147
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Size Change: +264 B (0%) Total Size: 7.95 MB 📦 View Changed
ℹ️ View Unchanged
|
|
Thanks for creating the PR, @adamsilverstein! Do we have any blockers here, or does it just require review/testing? I've also commented on the issue - #75145 (comment). |
No blockers. This seems to work reasonable well and isn't a huge change. its also backwards compatible - existing notes should migrate automatically. I fixed the focus issue after adding a note in 3b06990, the approach is a bit fragile so that would be good to improve if possible. This needs more testing and review. I will resolve the merge conflicts. |
3b06990 to
4ce3a1e
Compare
|
Thanks, @adamsilverstein! It looks like some checks are still failing. I also refactored the note selection state to handle focus, which is useful for 3rd-party consumers like keyboard shortcuts, palette commands, etc. Here's the PR - #75177. I would recommend rebasing this branch, and we can revisit the focus issue (3b06990). |
|
@Mamaduka thanks for the review and pointing to the focus fix. will update. |
b278cf2 to
eef844c
Compare
I rebased and reverted that commit. |
fa7091a to
12302b4
Compare
|
I've been thinking about this refactoring. Do you think it might be better to delay this one as well? Similar to inline notes. We haven't fully explored the new RTC features, which will be part of the next-gen notes. I think after 7.0, we will have a clearer picture of what's needed. P.S. I posted about punting inline notes earlier this week. |
Yes, I am happy to let this cook a little more before we release so we get it right. I wanted to explore the idea because my intuition told me this is the direction to go (array of ids) when we first decided to use note ids stored in blocks as our linking mechanism. I did fix all the failing CI tests and plan to do some additional manual testing. If it seems to work well and passes code review, maybe we can make it plugin-only so we can skip 7.0, but still get wider testing? |
I think it's okay to explore the idea in the plugin post for 7.0-beta. Similar attributes can be forward compatible; it's more code on our end, but the user won't notice with lazy migrations. P.S. I'll do another round of testing/reviews early next week. |
- `useNoteThreads`: numeric Map keys; drop `Number(noteId) ?? noteId` double-lookup - extract `pickPrimaryNote` (first unresolved else first); replace 3 inline copies in `index.js`/`notes.js` - auto-select effect: `prevBlockIdRef` change-detect; drop `selectedNoteRef` workaround - collapse `blockNoteIds`/`targetNoteId` into one `useMemo`
|
Pushed the following changes:
Speaking of e2e tests. @adamsilverstein, I think the following ones are mostly testing internals and now feature interactions. Which should already be covered by unit tests, do we want to keep them?
|
Nice!
I will double check they are already covered in unit testing then remove them, better and faster to test that way probably. |
Three e2e tests in block-notes.spec.js exercise pure metadata transforms or generic block serialization rather than user-visible behavior: - 'metadata noteId is cleaned up when all notes are deleted' duplicates removeNoteIdFromMetadata unit tests asserting noteId becomes undefined when the array empties (including from a legacy scalar). - 'lazy-migrates a legacy scalar noteId to an array' duplicates addNoteIdToMetadata unit tests covering scalar-to-array migration with legacy id preserved as the first entry. - 'multi-note metadata persists across save and reload' tests block-editor metadata serialization, not multi-notes-specific behavior; array shape and order preservation are pinned by the order-preservation unit tests. Drop them — unit tests run faster and cover the same invariants more precisely.
t-hamano
left a comment
There was a problem hiding this comment.
Apologies for the delayed review. While I'd like to look into the code details later, could we ensure that clicking the 'View notes' button in the block toolbar focuses on the new notes rather than the old ones? In other words, this is what I want to achieve.
Current behaviour:
My suggestion:
My guess is that users will likely want to see the most recent note threads first.
t-hamano
left a comment
There was a problem hiding this comment.
The current implementation relies on the order of elements in the array for sorting note threads, which might be a bit unstable. Theoretically, sorting by the first comment's post date for each note thread should ensure the correct order regardless of the array element order. What do you think?
This is an example of code generated by AI, although it has not been tested.
diff --git a/packages/editor/src/components/collab-sidebar/hooks.js b/packages/editor/src/components/collab-sidebar
/hooks.js
index 2f0eefff834..9a45259758f 100644
--- a/packages/editor/src/components/collab-sidebar/hooks.js
+++ b/packages/editor/src/components/collab-sidebar/hooks.js
@@ -112,11 +112,25 @@ export function useNoteThreads( postId ) {
}
// Single partition over notes-in-block-order. Each block can have
- // multiple note IDs, so iterate the flattened list.
+ // multiple note IDs; within a block, surface them by the root
+ // comment's date so display order is deterministic by creation
+ // time rather than by metadata insertion order. Id is used as a
+ // tiebreaker when two threads share a timestamp.
const unresolved = [];
const resolved = [];
for ( const noteIds of Object.values( blocksWithNotes ) ) {
- for ( const noteId of noteIds ) {
+ const sortedNoteIds =
+ noteIds.length > 1
+ ? noteIds.slice().sort( ( a, b ) => {
+ const dateA = threadsById.get( a )?.date_gmt ?? '';
+ const dateB = threadsById.get( b )?.date_gmt ?? '';
+ if ( dateA === dateB ) {
+ return a - b;
+ }
+ return dateA < dateB ? -1 : 1;
+ } )
+ : noteIds;
+ for ( const noteId of sortedNoteIds ) {
const thread = threadsById.get( noteId );
if ( ! thread ) {
continue;| it( 'handles string noteId (legacy format)', () => { | ||
| expect( getNoteIdsFromMetadata( { noteId: '42' } ) ).toEqual( [ | ||
| '42', | ||
| ] ); | ||
| } ); |
There was a problem hiding this comment.
Shouldn't the expected result be either [] or [ 42 ], not [ '42' ]? In the new format, the type of the element should always be a number.
There was a problem hiding this comment.
Good catch — fixed in 03bbfc4.
getNoteIdsFromMetadata now coerces values to finite positive numbers, so the new array format always contains numeric ids regardless of whether the legacy scalar was stored as a string. Test now expects [ 42 ].
As a side-benefit, the String() comparison shim in addNoteIdToMetadata / removeNoteIdFromMetadata and the redundant Number coercion in useNoteThreads are gone — there is now a single normalization point.
When I got into multiple users leaving notes on a block in #78218, i decided I like the order to be the same as the order of the text sections within the block. So the notes show up in the same order as the block selections -
Previously they were in reverse date order (most recent at top), but it was confusing when that didn't match the order of the text selections, so I changed it in bc8c9f8 |
getNoteIdsFromMetadata now coerces values to finite positive numbers, so the new array format always contains numeric ids regardless of whether legacy metadata stored a string. This drops the string/number String() comparison shim from addNoteIdToMetadata and removeNoteIdFromMetadata, and the redundant Number coercion from the useNoteThreads consumer.
Maybe this makes more sense for the pinned sidebar |
Refactor getNoteIdsFromMetadata, addNoteIdToMetadata, and removeNoteIdFromMetadata to use Set instead of array operations. Set preserves insertion order (so the first-note-is-block-aligned contract still holds) and also deduplicates the normalized output, which prevents the sidebar from rendering the same thread multiple times if metadata ever contains repeated ids. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
I see, in that case, let's leave it as it is 👍 |
t-hamano
left a comment
There was a problem hiding this comment.
I've pushed 43b9622, which refactors the helper functions with Set. This should simplify the logic. Additionally, during this process, I noticed an issue where getNoteIdsFromMetadata was not deduplicating, which should now be fixed. Please refer to the unit tests for this.
43b9622#diff-537b3b5e483d0787ddaaecdcd394331614c173bd8f05388d57d3a4e689837ae0R80-R93
@Mamaduka, I believe this PR is ready for ship. What do you think?
Mamaduka
left a comment
There was a problem hiding this comment.
Yes, let's merge this ✅
I would like to have another look at possible race conditions in CRUD actions, but that's a different problem. I'll do that when I have some free time.
Thanks, everyone, for working on this!
|
🎉 Woohoo! Thanks for the review and feedback here, this is a nice improvement! Merging. |
The suggest-mode stack diverged from trunk before #75147 landed, so addNoteIdToMetadata and getNoteIdsFromMetadata are absent from this branch's collab-sidebar/utils.js. suggestion-mode/provider.js imports both, breaking the JS bundle build (esbuild: no matching export) and cascading into the PHP, Playwright, and release checks. Vendor the two pure helpers verbatim from trunk (786c69c) so this PR builds independently of a full-stack rebase. They resolve cleanly against trunk's identical definitions when the stack is updated.
Commit d055691 vendored addNoteIdToMetadata/getNoteIdsFromMetadata onto this branch because the stale stack predated #75147. With trunk merged back into the stack, those helpers exist canonically in collab-sidebar/utils.js again, so the vendored copy is redundant and caused a duplicate-definition build break. Remove it; suggestion-mode provider.js now imports the trunk-supplied versions.

What?
Enables multiple independent note threads to be attached to a single block. Previously, each block could only have one top-level note, with subsequent notes forced to be replies.
Closes #75145
Why?
Unlocks potential fixes for #74751 - Real-time collaboration race condition where two users adding notes simultaneously causes one to be orphaned. Note ids can now be merged when added ~silmotaneously.
Unblocks #74852 - Partial text selection notes feature requires multiple threads per block.
Forward-compatible with #73416 (cross-block notes)
The array shape introduced here also serves the cross-block notes case in #73416 without a second attribute migration. A note that spans blocks A → C registers the same note id in each block's
noteIdsarray; reads will resolve all three blocks back to the single thread. No further migration ofmetadata.noteIdis anticipated when #73416 lands.How?
Extends the existing
noteIdmetadata field to support either a scalar (legacy) or array format:Core Changes
Utility functions (
utils.js):getNoteIdsFromMetadata()- normalizes both formats to arrayaddNoteIdToMetadata()- appends to array, prevents duplicatesremoveNoteIdFromMetadata()- removes specific ID from arrayHook updates (
hooks.js):useBlockComments()- builds array-based block→notes mappingsonCreate()- appends new noteId instead of overwritingonDelete()- removes specific noteId, usescomment.blockClientIdfor correct targetingSidebar components (
index.js,comments.js):Backwards Compatibility
noteId: 42and arraynoteId: [1, 2, 3]Testing Instructions
Automated Tests
npm run test:unit -- --testPathPattern=collab-sidebarAI usage
I used Claude code extensively to help me analyze the current approach and come up with a plan. Claude wrote the initial Issue which I modified by adding some acceptance criteria, then passed back to Claude to develop an implementation plan. Claude write the code which I then reviewed. Claude created the PR which I then edited.
Next, I asked Code Rabbit to review the code changes locally.
Screenshots or screencast
Screen.Recording.2026-02-05.at.1.12.44.PM.mov
*the focus issue was fixed after this recording.
*a block can contain one or more attached notes
Authored with help from Claude Code