Skip to content

Add a Notes section in the Dashboard Activity widget - #13076

Open
mcrisp1972 wants to merge 41 commits into
WordPress:trunkfrom
mcrisp1972:ticket/65890
Open

mcrisp1972 wants to merge 41 commits into
WordPress:trunkfrom
mcrisp1972:ticket/65890

Conversation

@mcrisp1972

@mcrisp1972 mcrisp1972 commented Aug 16, 2026 •

Copy link
Copy Markdown

We are adding notes posted to blocks in the editor to the Recent Activity widget in the admin dashboard.

Trac ticket: https://core.trac.wordpress.org/ticket/65890

Testing

Tool to create many notes: https://github.com/t-hamano/notes-data-generator

https://playground.wordpress.net/wordpress.html?pr=13076

Screenshot

image

More screenshots

Claude seeded a site with notes and captured the widget in each state:

The test site has six posts carrying note threads: several threads have replies, some threads are resolved, one post has every thread resolved, and one post belongs to another author.

The Activity widget with five posts that have open notes. Each row links to the post, counts the open threads on it, and is dated by the newest note or reply on that post, in the same date, title, count order the sections above it use.

Activity widget showing the Recent Notes section with five posts

Where those counts come from. The same post in the editor: four open threads, two of them with replies, and the resolved threads below the divider. The dashboard reads "4 open notes" for it.

Editor sidebar listing four open note threads and the resolved ones

Eight more states

A post with one open thread uses the singular string.

Recent Notes listing a single post with one open note

Every thread resolved on every post, so the Notes section is not rendered at all and the rest of the widget is unchanged.

Activity widget with no Recent Notes section

A thread replied to minutes ago on a six-day-old note sorts to the top, dated by the reply rather than by the note that opened the thread.

Recent Notes ordered by the most recent reply

Dates across the year: today, earlier this year, and last year, which picks up the year.

Recent Notes rows dated today, earlier this year, and last year

Long titles, a post with 24 open notes, and an untitled draft in the same list.

Recent Notes with a wrapping title, a 24 note count, and a no title row

Signed in as an author who can only edit their own post: the posts they cannot edit drop out of the list, notes and all.

Activity widget for an author, listing only their own post

At 420px wide, the date and the count hold their columns and the title wraps into what is left.

Activity widget at mobile width

The whole dashboard for context.

Full dashboard with the Activity widget

Commit message

Claude drafted this core commit message for review:

Editor: Add a Notes section to the Activity widget.

Add a Recent Notes section to the Dashboard Activity widget, after Publishing Soon, Recently Published and Recent Comments, that lists up to five posts with open notes. Each row links to the post editor, shows how many threads are open on the post, and is dated by its newest note or reply.

Only posts the current user can `edit_post` are listed, which is the same rule the REST API applies to notes. A reply counts as recent activity only while its thread is open. Note queries are paged with a cap, so a user who can edit few posts doesn't end up scanning every note on the site. When there are no open notes the section is left out and the widget looks the same as before. It is read-only: there are no moderation actions, and it doesn't link to individual notes.

Props mcrisp1972, ancawonka, aosmichenko, westonruter, annezazu.
Fixes #65890.

@github-actions

github-actions Bot commented Aug 16, 2026 •

Copy link
Copy Markdown

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 props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props mcrisp1972, ancawonka, aosmichenko, adamsilverstein, westonruter, annezazu.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

aosmichenko and others added 7 commits August 16, 2026 13:36
WP_Comment exposes its columns as strings, so passing comment_post_ID
straight to _draft_or_post_title() and get_edit_post_link() fails static
analysis. It also repeated an error pattern the PHPStan baseline records
once for the comments widget, pushing that entry out of date.
Comment thread src/wp-admin/includes/dashboard.php Outdated
@adamsilverstein adamsilverstein changed the title Ticket/65890 Add a Notes section in the Dashboard Activity widget Aug 16, 2026
Tallying the open notes of the displayed posts loaded every one of those
notes just to count rows, an unbounded query on a busy post. Query each
post's count instead, which returns a single value and skips priming the
comment and meta caches.
Covers the edit link, the post title and its placeholder, escaping, the
open note count and its singular and plural forms, and the relative date
each row ends with, including its use of the timezone of the site.

The dashboard had no test coverage of its own before this.
…old.

A note is created on hold and is approved when it is resolved, so the row
tests now create their notes the way the editor does.

Adds coverage of the listing itself: the open note count, which posts are
listed and in what order, and the exclusion of resolved threads, replies
and regular comments.
The section queried the notes that start a thread and dated each post by
one of those, so a thread opened weeks ago and replied to this morning
was dated weeks ago, and sorted below a thread nobody had touched since
it was opened.

Query replies too, and date each post by the most recent note or reply in
a thread that is still open. Resolving a thread approves the note that
starts it while its replies stay on hold, so the thread a reply belongs to
is checked before the reply counts as activity. The threads are primed in
one query per page rather than fetched one at a time.

Also pass 'orderby' => 'none' to the open note counts, which do not need
the ordering, matching get_comment_count().
@adamsilverstein

adamsilverstein commented Aug 16, 2026 •

Copy link
Copy Markdown
Member

Working on this a few points emerged:

  • Rather than showing recent Notes, we decided to show the Posts with the most recent notes or replies. There is no "Notes" screen to link to, when you have a new note, the main action is to view the note on the edit post screen.
  • Showing the most recent date is tricky, we need only unresolved threads, which is recorded at the top level note - but we still want the date from the most recent reply. This complicated querying for the notes to get what we needed.

Notes are only listed for posts the current user can edit, so a user who
can edit none of them paged through every open note on the site, one
query per page. Stop paging once a hundred notes per row have been
examined, and prime the posts the capability checks load while skipping
the note meta they never read.
The row put the post title in the narrow first column the sibling
sections use for the date, so a title of any length wrapped while the
open note count stretched across the free space. Render the date first,
the title in the column that grows, and the count last, and drop a
duplicated block of rules left over from an earlier iteration.

Skip a row whose post no longer has an edit link along the way, as
esc_url() is passed null for a post deleted since the notes were
queried.
@adamsilverstein

Copy link
Copy Markdown
Member

I had Claude review the branch, and pushed what it turned up:

Two commits, both on top of the existing work here.

d4d4678 bounds the paging loop. Notes are only listed for posts the current user can edit, so a user who can edit none of them - a Subscriber, or an Author on a multi-author site - never satisfied the while condition and paged through every open note on the site, 50 at a time. On a busy site that is a lot of get_comments() calls per dashboard load. Paging now stops after a hundred notes per row have been examined. The same commit adds update_comment_post_cache (the current_user_can( 'edit_post', ... ) check was pulling one get_post() query per note) and turns off update_comment_meta_cache, which was priming meta on every scanned note that nothing ever reads.

c8c9b7f is the row layout. The markup was title, count, date, so the title landed in the narrow clamped column that #published-posts reserves for the date, and the "4 open notes" string stretched across the free space instead. Any title of normal length wrapped in a ~170px column with a wide gap next to it. Rows are now date, title, count, which lines the Notes section up with Recently Published directly above it - see the refreshed screenshots in the description. That commit also drops a duplicated #latest-notes block at the bottom of dashboard.css (every declaration in it was already set earlier in the file), and guards get_edit_post_link(), which returns null for a post deleted between the query and the render and was reaching esc_url() as a PHP 8.1 deprecation.

Tests came along with each: the paging bound, the edit link guard, and the two row helpers that read the date out of the markup. --group notes,dashboard,admin,comment is green at 1657 tests, and PHPCS and PHPStan are clean.

One review note that turned out to be a non-issue, for the record: orderby looked like it needed a comment_ID tiebreaker for stable paging, but WP_Comment_Query already appends one to any orderby - see https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/class-wp-comment-query.php#L672-L703. Nothing to do there.

@mcrisp1972 does the date-first row order look right to you? Happy to put it back if you had a reason for leading with the title.

@mcrisp1972

Copy link
Copy Markdown
Author

I had Claude review the branch, and pushed what it turned up:

Two commits, both on top of the existing work here.

d4d4678 bounds the paging loop. Notes are only listed for posts the current user can edit, so a user who can edit none of them - a Subscriber, or an Author on a multi-author site - never satisfied the while condition and paged through every open note on the site, 50 at a time. On a busy site that is a lot of get_comments() calls per dashboard load. Paging now stops after a hundred notes per row have been examined. The same commit adds update_comment_post_cache (the current_user_can( 'edit_post', ... ) check was pulling one get_post() query per note) and turns off update_comment_meta_cache, which was priming meta on every scanned note that nothing ever reads.

c8c9b7f is the row layout. The markup was title, count, date, so the title landed in the narrow clamped column that #published-posts reserves for the date, and the "4 open notes" string stretched across the free space instead. Any title of normal length wrapped in a ~170px column with a wide gap next to it. Rows are now date, title, count, which lines the Notes section up with Recently Published directly above it - see the refreshed screenshots in the description. That commit also drops a duplicated #latest-notes block at the bottom of dashboard.css (every declaration in it was already set earlier in the file), and guards get_edit_post_link(), which returns null for a post deleted between the query and the render and was reaching esc_url() as a PHP 8.1 deprecation.

Tests came along with each: the paging bound, the edit link guard, and the two row helpers that read the date out of the markup. --group notes,dashboard,admin,comment is green at 1657 tests, and PHPCS and PHPStan are clean.

One review note that turned out to be a non-issue, for the record: orderby looked like it needed a comment_ID tiebreaker for stable paging, but WP_Comment_Query already appends one to any orderby - see https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/class-wp-comment-query.php#L672-L703. Nothing to do there.

@mcrisp1972 does the date-first row order look right to you? Happy to put it back if you had a reason for leading with the title.

@adamsilverstein

I had Claude review the branch, and pushed what it turned up:

Two commits, both on top of the existing work here.

d4d4678 bounds the paging loop. Notes are only listed for posts the current user can edit, so a user who can edit none of them - a Subscriber, or an Author on a multi-author site - never satisfied the while condition and paged through every open note on the site, 50 at a time. On a busy site that is a lot of get_comments() calls per dashboard load. Paging now stops after a hundred notes per row have been examined. The same commit adds update_comment_post_cache (the current_user_can( 'edit_post', ... ) check was pulling one get_post() query per note) and turns off update_comment_meta_cache, which was priming meta on every scanned note that nothing ever reads.

c8c9b7f is the row layout. The markup was title, count, date, so the title landed in the narrow clamped column that #published-posts reserves for the date, and the "4 open notes" string stretched across the free space instead. Any title of normal length wrapped in a ~170px column with a wide gap next to it. Rows are now date, title, count, which lines the Notes section up with Recently Published directly above it - see the refreshed screenshots in the description. That commit also drops a duplicated #latest-notes block at the bottom of dashboard.css (every declaration in it was already set earlier in the file), and guards get_edit_post_link(), which returns null for a post deleted between the query and the render and was reaching esc_url() as a PHP 8.1 deprecation.

Tests came along with each: the paging bound, the edit link guard, and the two row helpers that read the date out of the markup. --group notes,dashboard,admin,comment is green at 1657 tests, and PHPCS and PHPStan are clean.

One review note that turned out to be a non-issue, for the record: orderby looked like it needed a comment_ID tiebreaker for stable paging, but WP_Comment_Query already appends one to any orderby - see https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/class-wp-comment-query.php#L672-L703. Nothing to do there.

@mcrisp1972 does the date-first row order look right to you? Happy to put it back if you had a reason for leading with the title.

@adamsilverstein , Jen

Update the layout and view

@adamsilverstein , this is looking good. Regarding the ordering of row date and title, Jen and @aosmichenko built out the rows, so we might want their input on that, but I think the date first makes sense, especially if that is the sorted field.

@aosmichenko

aosmichenko commented Aug 18, 2026 •

Copy link
Copy Markdown

I was initially thinking of making "time ago" format, so if you would rather do that instead. I have no hard preferences on the format and column order, was trying to find the most useful to read.

@adamsilverstein

Copy link
Copy Markdown
Member

I was initially thinking of making "time ago" format, so if you would rather do that instead. I have no hard preferences on the format and column order, was trying to find the most useful to read.

I like the human readable time ago format, we use that for most user facing dates.

Review on the PR favored the relative "time ago" format over the
Today/Tomorrow/date wording, matching how most user facing dates are
shown elsewhere in the admin, such as the Media list table and the
revisions screen.

Compute the difference from the GMT date of the note so it is correct
in any site timezone, which also removes the dead branch guarding a
non-integer timestamp: get_comment_date( 'U' ) always returned one.
The separate clock time is dropped because the difference already
carries it, taking the '%1$s, %2$s' join and the date format strings
out of the translation catalog. The row markup keeps its shape, so the
column layout and dashboard.css are unchanged.
Comment thread src/wp-admin/includes/dashboard.php Outdated
Comment thread src/wp-admin/includes/dashboard.php
Comment thread src/wp-admin/includes/dashboard.php Outdated
Comment thread src/wp-admin/includes/dashboard.php Outdated
Comment thread src/wp-admin/includes/dashboard.php Outdated
Comment thread src/wp-admin/includes/dashboard.php Outdated
adamsilverstein and others added 2 commits October 17, 2026 15:32
Co-authored-by: Weston Ruter <westonruter@gmail.com>
The row helper was a new global function with a single caller, which adds
API surface core would have to keep. Rendering the rows in
wp_dashboard_recent_notes() with a template also reads closer to the
markup it produces than a printf() format string.

The edit link check moves into the selection loop, so a post without one
no longer takes up a row slot and leaves the section short. The row tests
fold into the section tests, matching the markup whitespace-tolerantly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A71VCCi5zWeU1wpawPZqnx
@annezazu

Copy link
Copy Markdown

I'm wondering if we should add a default Note here as well so we would get a "Recent Notes" section (which would help with feature deiscovery). What do you think?

That's a great idea to add it to the usual default intro post so folks can help discover it. Let's try it!

Comment thread src/wp-admin/includes/dashboard.php Outdated
@adamsilverstein

Copy link
Copy Markdown
Member

I'm wondering if we should add a default Note here as well so we would get a "Recent Notes" section (which would help with feature deiscovery). What do you think?

That's a great idea to add it to the usual default intro post so folks can help discover it. Let's try it!

Ok, I'll add this!

`strtotime()` returns false for a date it cannot parse, and passing that to `human_time_diff()` is flagged by PHPStan. Skip the row instead, as there is no meaningful time to show for it.

Claude-Session: https://claude.ai/code/session_01Dww9xkqg9XGQfHH3jwwSZo
A fresh install shows the default comment under Recent Comments in the Activity dashboard widget, but Recent Notes stays empty, so nothing points new users at Notes. Add an open note by the site admin and link it to the first block of the "Hello world!" post through the block's `metadata.noteId` attribute, so the note shows in the widget and on its block in the editor.

No note is added when the first post has no blocks to attach it to, such as a multisite network's plain text first post option.

Claude-Session: https://claude.ai/code/session_01Dww9xkqg9XGQfHH3jwwSZo
@adamsilverstein

Copy link
Copy Markdown
Member

Thanks @annezazu, glad the idea made sense. Fresh installs now get a default note so Recent Notes shows up alongside Recent Comments.

Claude Code wired this up and wrote the summary:

Added in dcef80f. wp_install_defaults() now adds an open note by the site admin to the "Hello world!" post, right after the default comment. The note reads:

Hi, this is a note. Notes let you leave feedback on a block for anyone else working on this post. To add one, select a block and choose Add note from its options menu. Resolve a note once its feedback has been addressed.

The note is linked to the post's first block through the block's metadata.noteId attribute (the same link the editor writes when a note is added), so it shows on the paragraph in the editor rather than floating loose. It is stored like an editor-created note: comment_type note, unapproved until resolved, so it counts as an open note in the widget. Notes are already left out of comment counts, so the Pending bubble and the post's comment count stay unchanged.

One multisite caveat: the network's default first_post option is plain text with no block markup, so there is no block to attach a note to and new subsites skip the default note. Sites whose network admin cleared the option (which falls back to a paragraph block) or uses block markup get it on the first block.

Fresh install, Activity widget:

Activity widget on a fresh install showing Recent Notes with Hello world! and 1 open note

Same install, the note in the editor with its paragraph selected:

Hello world! post in the editor with the All notes panel open showing the default note

Tests for the note are in tests/phpunit/tests/multisite/wpInstallDefaults.php, since wp_install_defaults() writes fixed post IDs and can only be run again in a test through a new site.

Should we also turn the multisite default first_post into block markup so subsites get the note too, or leave that for a follow up?

Comment thread src/wp-admin/includes/upgrade.php Outdated
@adamsilverstein

Copy link
Copy Markdown
Member

Hi, this is a note. Notes let you leave feedback on a block for anyone else working on this post. To add one, select some text or an entire block and choose Add note from its options menu. Resolve a note once its feedback has been addressed.

westonruter and others added 10 commits October 17, 2026 21:47
…null`

Fixes:

 ------ ---------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-admin/includes/dashboard.php
 ------ ---------------------------------------------------------------------------------------------------------------------------------------------------
  1316   Parameter WordPress#1 $url of function esc_url expects string, string|null given.
         🪪  argument.type
 ------ ---------------------------------------------------------------------------------------------------------------------------------------------------
Fixes the missingType.return PHPStan errors reported for
`wpSetUpBeforeClass()` and each test method in
`Admin_Includes_Dashboard_WpDashboardRecentNotes_Test`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The helper returned a bare `array`, so every value destructured from it
was `mixed`, failing the string parameters of `get_linked_titles()`,
`assertMatchesRegularExpression()`, and `substr_count()`. Return
`array{0: bool, 1: string}` instead, casting the buffer like `get_echo()`
does, and declare `$total_items` as `positive-int` to match
`wp_dashboard_recent_notes()`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es test

`get_edit_post_link()` returns `null` when the post type does not exist,
which `esc_url()` does not accept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`get_post()` returns `null` for a missing post, so the helper did not
satisfy its `WP_Post` return shape, and reading `comment_count` from it
directly accessed a property on a possibly-null value. Assert the
instance first, after restoring the current blog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Parsed block attributes are untyped, so reaching into
`['metadata']['noteId']` accessed offsets on `mixed`. Asserting the
entire `attrs` array avoids that, and is stricter too: it also checks that
no other attributes were added to the block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Declares the types already documented in the docblocks of the helpers in
the `wp_dashboard_recent_notes()` and multisite install defaults tests,
plus the `get_edit_post_link` filter callback, resolving PhpStorm's
"Missing parameter's type declaration" inspection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Declares the return types already documented on the test helpers, and
types the static user ID properties as `int`, resolving PhpStorm's
"Missing function's return type declaration" and "Missing property's
type declaration" inspections. The factory's `create()` now throws
rather than returning a `WP_Error`, so the `int` return types are safe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@westonruter

Copy link
Copy Markdown
Member

I've fixed the PHPStan errors in the new PHPUnit tests.

@westonruter westonruter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Comment by Claude Opus 5.5

Findings from a medium-effort code review of the branch for Core-65890. They are left as inline comments, roughly in order of severity:

  1. The examined-notes cap can hide a user's own open notes entirely (dashboard.php L1199).
  2. A $total_items of 0 removes the query limit (L1182).
  3. Paging keeps querying after a short page, and offsets can drift (L1259).
  4. The capability check is redundant with get_edit_post_link(), and the link is computed twice (L1248).
  5. Rejected posts are re-checked for every one of their notes (L1248).
  6. One COUNT query per displayed post (L1275).
  7. Future-dated or zero-dated notes get a misleading relative time (L1308).
  8. serialize_blocks() re-encodes the whole network-supplied first_post (upgrade.php L357).
  9. The default note text cannot be customized per network (upgrade.php L338).

Item 1 changes behavior that test_should_stop_paging_after_a_hundred_notes_per_row currently asserts, so it is a design question as much as a bug.

* that the section costs a bounded number of queries however many notes the
* site has.
*/
$max_notes_examined = $total_items * 100;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Comment by Claude Opus 5.5

The cap can silently hide a user's own open notes, and the section then reports that there are none.

An Author can only edit their own posts. If 500 or more newer open notes sit on other people's posts (100 or more when $total_items is 1), paging stops before reaching the Author's notes. The function returns false and no Recent Notes block appears, even though the Author has open notes. test_should_stop_paging_after_a_hundred_notes_per_row currently asserts exactly this outcome.

Scoping the query to posts the user can edit, instead of paging past the ones they cannot, would avoid the miss. For example, restrict to post_author for users without edit_others_posts, or use a post__in of editable posts.

'status' => 'hold',
'orderby' => 'comment_date_gmt',
'order' => 'DESC',
'number' => $total_items * 5,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Comment by Claude Opus 5.5

The native int type from 5fe68ed rules out strings, but 0 still gets through. 'number' => 0 is treated by WP_Comment_Query as no limit (absint() gives 0, so no LIMIT clause is added), so every open note on the site is loaded.

$max_notes_examined is then also 0, so the loop exits after that single pass. Since count( $latest_notes ) === 0 is never true, every post with an open note is rendered instead of none.

The positive-int in the docblock is not enforced at runtime. An early if ( $total_items < 1 ) { return false; } would guard against it.

}
}

$notes_query['offset'] += $notes_query['number'];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Comment by Claude Opus 5.5

When a page comes back with fewer rows than 'number', the loop still runs another get_comments() at the next offset, which is bound to be empty. Breaking when count( $possible ) < $notes_query['number'] saves that round trip.

Separately, paging by offset over a live, date-ordered result set can skip a note if one is added or resolved between page queries. That is probably acceptable for a dashboard widget, but worth noting.

* Notes are only visible to users who can edit the post they belong to,
* and each row links to the post's edit screen.
*/
if ( ! current_user_can( 'edit_post', $note_post_id ) || ! get_edit_post_link( $note_post_id ) ) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Comment by Claude Opus 5.5

current_user_can( 'edit_post', $note_post_id ) is redundant here, because get_edit_post_link() already returns null when the user cannot edit the post (see link-template.php).

The link is also computed again in the render loop (L1300), where the ! $edit_post_link branch can no longer be reached. Storing the link alongside the note in $latest_notes here would drop both the duplicate capability check and the second get_edit_post_link() call per row.

Also, posts that fail this check are not remembered. With 100 open notes on a post the user cannot edit, the capability and link checks run 100 times for the same post ID. Keeping a set of skipped post IDs, checked next to the isset( $latest_notes[ $note_post_id ] ) short-circuit, would reduce that to one check per post.

$open_notes = array();

foreach ( array_keys( $latest_notes ) as $note_post_id ) {
$open_notes[ $note_post_id ] = (int) get_comments(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Comment by Claude Opus 5.5

The comment above explains why a count query per post is preferred over loading the notes. Even so, this still costs up to $total_items extra queries on every dashboard load that has notes.

A single grouped query would return all the counts in one round trip without loading any notes. It would select comment_post_ID, COUNT(*) with comment_post_ID IN (…), comment_type = 'note', comment_parent = 0 and comment_approved = '0', grouped by comment_post_ID.

$note_post_title = _draft_or_post_title( $note_post_id );

/* translators: %s: Human-readable time difference. */
$note_time_diff = sprintf( __( '%s ago' ), human_time_diff( $note_time ) );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Comment by Claude Opus 5.5

Minor: human_time_diff() gets a single argument, and the label is always "%s ago". So a note whose comment_date_gmt is ahead of the server clock (clock skew, or imported data) is shown as "X mins ago". A zero date (0000-00-00 00:00:00) is shown as "56 years ago".

The is_int() guard on L1296 never catches either case, because strtotime() returns an int for both.


$wpdb->update(
$wpdb->posts,
array( 'post_content' => serialize_blocks( $first_post_blocks ) ),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Comment by Claude Opus 5.5

Running the network-supplied first_post through parse_blocks() and then serialize_blocks() re-encodes the attributes of every block, not only the one being annotated. Characters such as &, <, > and -- come out as \u0026 and so on, and comment-delimiter spacing is normalized. The stored post content then no longer matches the configured network option byte for byte.

Rewriting only the first block's opening delimiter, for example with WP_Block_Processor or a targeted replacement of that one delimiter, would leave the rest of the content untouched.

'comment_author_url' => $note_author->user_url,
'comment_date' => $now,
'comment_date_gmt' => $now_gmt,
'comment_content' => __( 'Hi, this is a note. Notes let you leave feedback on a block for anyone else working on this post. To add one, select some text or an entire block and choose Add note from its options menu. Resolve a note once its feedback has been addressed.' ),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Comment by Claude Opus 5.5

On multisite, every other piece of default content (first_post, first_page, first_comment, and so on) can be overridden through a network option. The default note text cannot.

A network that customizes first_post still gets this instructional note on every new site's first post, with its noteId written into the network's custom content. Short of overriding the pluggable wp_install_defaults(), there is no way to change or suppress it. A first_note network option, or at least a filter, would bring it in line with the rest.

Comment on lines +348 to +351
$first_block_meta = isset( $first_block_attrs['metadata'] ) && is_array( $first_block_attrs['metadata'] ) ? $first_block_attrs['metadata'] : array();

$first_block_meta['noteId'] = $wpdb->insert_id;
$first_block_attrs['metadata'] = $first_block_meta;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Note there are two PHPStan errors on these lines:

 ------ ---------------------------------------------------------------------------------------------------------------------------------------------------
  Line   src/wp-admin/includes/upgrade.php
 ------ ---------------------------------------------------------------------------------------------------------------------------------------------------
  348    Cannot access offset 'metadata' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
  351    Cannot access offset 'metadata' on mixed.
         🪪  offsetAccess.nonOffsetAccessible
 ------ ---------------------------------------------------------------------------------------------------------------------------------------------------

They are being addressed separately in #14457

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants