Repository navigation
Add emoji reactions (limited set on notes) - #76767
adamsilverstein wants to merge 196 commits into
Conversation
|
Size Change: +2.71 kB (+0.03%) Total Size: 7.92 MB 📦 View Changed
|
|
Flaky tests detected in 4b59106. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/33358611256 As a user I expect my navigation to use the colors I selected for it in
|
|
Regarding unbounded requests & embedding reactions: If embedding all the reactions on a given note is too much, we could think about caching/retrieving only the reaction count (grouped by emoji). This way we can quickly display the different reactions on a note and lazy load the individual reactions (and who gave them) after that. |
|
Two open threads carried over from #75549 worth noting:
All other feedback from #75549 has been addressed — see my comparison for details. |
|
@swissspidy this is what's shipping now - in case you want to double-check it matches what you had in mind:
The unbounded |
|
I'm going to look at leveraging an existing library for the emoji picker itself, something like https://github.com/liveblocks/frimousse that is well maintained, license compatible and designed to be styled/extended. This would provide a much richer experience, and we can still start with the simplified design of a small set of emoji with the full selection "behind" a (+) plus button. |
|
Here is a screencast of the current implementation in action: react.with.emoji.picker.mp4 |
Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com>
…tions-try-addditional-comment-type # Conflicts: # packages/editor/CHANGELOG.md
Reviewers on the matching core change prefer not to define what counts as an "internal" comment type before a custom comment types API exists, since reactions may later apply to public comments too. Drop the gutenberg_get_internal_comment_types() helper and its filter, and list 'note' and 'reaction' at each call site. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01THCU4aj11hnJP5ZbUegTRh
…on-display.tsx Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com>
The picker now uses checkbox items so users can see which emoji they already reacted with. On a resolved thread the add-reaction trigger was shown disabled on some notes and hidden until hover on others; hiding it entirely removes that inconsistency, since reactions can't be toggled there anyway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01THCU4aj11hnJP5ZbUegTRh
The pill's accessible label already formats the count for the user's locale, so the visible number should match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01THCU4aj11hnJP5ZbUegTRh
…dditional-comment-type' into add-notes-emoji-reactions-try-addditional-comment-type
Reviewers on the matching core change prefer not to define what counts as an "internal" comment type before a custom comment types API exists, since reactions may later apply to public comments too. Drop the gutenberg_get_internal_comment_types() helper and its filter, and list 'note' and 'reaction' at each call site, as on #76767. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01THCU4aj11hnJP5ZbUegTRh
Format the count for the user's locale in both the pill and its accessible label, as on #76767. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01THCU4aj11hnJP5ZbUegTRh
The trashed and resolved checks ran before the same-post check, so a user who can edit one post could pass a note ID from another post and learn from the error whether that note was trashed or resolved. Check the post first so a note elsewhere always gets the same error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017a8BrMHjzF5oUjpCyXWoRu
…tions-try-addditional-comment-type # Conflicts: # packages/editor/CHANGELOG.md # packages/editor/src/components/collab-sidebar/README.md
The core backport picked up review fixes the plugin controller lacked: reactions can only be created approved and skip the status handler, the reaction key is trimmed before validation, a note whose only children are reactions no longer advertises a children link, and the collection permission check follows core's handling of post types without notes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013G96dwN6H2B8pDSyrryfd5
Core's backport excludes reactions unless 'all' or 'reaction' is asked for via type or type__in, and keeps notes and reactions out of get_lastcommentmodified(). The plugin only excluded them when no type was set and never touched the last modified lookup. Reaction IDs are also returned oldest first, matching wp_get_note_reaction_ids(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013G96dwN6H2B8pDSyrryfd5
| * | ||
| * @since 7.2.0 | ||
| * | ||
| * @return array Item schema data. |
There was a problem hiding this comment.
| * @return array Item schema data. | |
| * @return array<string, mixed> Item schema data. |
| 'type' => 'object', | ||
| 'context' => array( 'view', 'edit' ), | ||
| 'readonly' => true, | ||
| 'additionalProperties' => array( |
There was a problem hiding this comment.
It's not clear to me why additionalProperties is used here. Couldn't it enumerate the emoji for each property explicitly? Maybe not if it is unbounded.
| 'properties' => array( | ||
| 'count' => array( | ||
| 'description' => __( 'Total number of reactions with this emoji.', 'gutenberg' ), | ||
| 'type' => 'integer', |
There was a problem hiding this comment.
| 'type' => 'integer', | |
| 'type' => 'integer', | |
| 'minimum' => 0, |
| ), | ||
| 'current_user_reaction' => array( | ||
| 'description' => __( 'The current user\'s reaction comment ID for this emoji, or 0 if they have not reacted.', 'gutenberg' ), | ||
| 'type' => 'integer', |
There was a problem hiding this comment.
| 'type' => 'integer', | |
| 'type' => 'integer', | |
| 'minimum' => 0, |
| * @param string $type The comment type from the request. | ||
| * @return bool True if the type is one of the internal comment types. | ||
| */ | ||
| protected function is_note_or_reaction( $type ) { |
There was a problem hiding this comment.
| protected function is_note_or_reaction( $type ) { | |
| protected function is_note_or_reaction( ?string $type ): bool { |
| $new_count = (int) $wpdb->get_var( | ||
| $wpdb->prepare( | ||
| // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared | ||
| "SELECT COUNT(*) FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved = '1' AND comment_type NOT IN ( $type_placeholders )", |
There was a problem hiding this comment.
No need to use $type_placeholders here. The $internal_types is an array of string literals which can be used in the SQL directly.
| * includes trashed reactions. | ||
| * @return int[] Reaction comment IDs, oldest first. | ||
| */ | ||
| function gutenberg_get_note_reaction_ids( $note, $status = 'any' ) { |
There was a problem hiding this comment.
| function gutenberg_get_note_reaction_ids( $note, $status = 'any' ) { | |
| function gutenberg_get_note_reaction_ids( WP_Comment $note, string $status = 'any' ): array { |
| * @param string $comment_id The comment ID as a numeric string. | ||
| * @param WP_Comment $comment The comment being deleted. | ||
| */ | ||
| function gutenberg_delete_note_reactions( $comment_id, $comment ) { |
There was a problem hiding this comment.
| function gutenberg_delete_note_reactions( $comment_id, $comment ) { | |
| function gutenberg_delete_note_reactions( $comment_id, WP_Comment $comment ): void { |
| * @since 7.2.0 | ||
| * | ||
| * @param string $comment_id The comment ID as a numeric string. | ||
| * @param WP_Comment $comment The comment being deleted. |
There was a problem hiding this comment.
| * @param WP_Comment $comment The comment being deleted. | |
| * @param WP_Comment $comment The comment being deleted. | |
| * | |
| * @phpstan-param numeric-string $comment_id |
| * @param string $comment_id The comment ID as a numeric string. | ||
| * @param WP_Comment $comment The trashed comment. | ||
| */ | ||
| function gutenberg_trash_note_reactions( $comment_id, $comment ) { |
There was a problem hiding this comment.
| * @param string $comment_id The comment ID as a numeric string. | |
| * @param WP_Comment $comment The trashed comment. | |
| */ | |
| function gutenberg_trash_note_reactions( $comment_id, $comment ) { | |
| * @param string $comment_id The comment ID as a numeric string. | |
| * @param WP_Comment $comment The trashed comment. | |
| * | |
| * @phpstan-param numeric-string $comment_id | |
| */ | |
| function gutenberg_trash_note_reactions( $comment_id, WP_Comment $comment ): void { |
- useNoteReactions( noteId ): useEntityProp-style [ summary, toggle ]. - Drop redundant summary refetch; notes list refetch already brings it. - Merge pills and picker into NoteReactions. - Reactor names tied to summary identity; drop module cache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Pushed a cleanup commit for the reactions JS; no user-visible change. Each toggle was sending three requests: the reaction save/delete, a
Follow-ups, not blocking:
|
Replace the hand-rolled page walk and names cache with a getEntityRecords select gated on pill hover/focus. core-data caches across remounts and invalidates on comment writes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
One more follow-up: reactor names in the pill tooltip now load through core-data. Each pill selects The query includes |
| * @package gutenberg | ||
| * @since 7.2.0 |
There was a problem hiding this comment.
| * @package gutenberg | |
| * @since 7.2.0 | |
| * @package gutenberg |
Reading this header, the class looks like new code introduced in 7.2. In practice, it's a compat layer that overrides core's WP_REST_Comments_Controller so the plugin can support reactions on older WordPress versions.
Because of that, a change needed in just one line of a core method means overriding the whole method here. Therefore, some of the review feedback on this file is about existing core code rather than what this PR introduces.
To make review easier, how about marking with comments where this PR actually changes code inside each method? For example:
public function get_items_permissions_check( $request ) {
// START CHANGE: Treat `reaction` like `note`.
$is_note = $this->is_note_or_reaction( $request['type'] );
// END CHANGE.
$is_edit_context = 'edit' === $request['context'];There was a problem hiding this comment.
how about marking with comments where this PR actually changes code inside each method?
will do.
There was a problem hiding this comment.
Claude made this change, quick note from it:
Added START CHANGE / END CHANGE markers around every line that differs from core (WordPress 7.1.0) and applied the header suggestion in ae52d25.
…on-emojis.ts Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com>
The controller copies whole core methods to change a few lines, which made it hard for reviewers to tell reaction changes from existing core code. Wrap every divergence from WordPress 7.1.0 in START CHANGE / END CHANGE comments, restore core's wording elsewhere in the copies, and reword the header so the class reads as a compat override. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0126YH7wfjdvbPpuuNv1v92h
WordPress coding standards call for /* */ on comments that span more than one line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0126YH7wfjdvbPpuuNv1v92h
Address review feedback: add return and parameter types to the new helper methods, a full shape for the cached summaries, a minimum of 0 on the summary integers, and drop empty() checks the reaction code does not need. Inline the constant reaction type and status in the summary queries so only the note IDs go through prepare(). is_note_or_reaction() keeps an untyped parameter: create requests do not validate 'type', so a string hint would turn ?type[]= into a fatal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0126YH7wfjdvbPpuuNv1v92h
The excluded comment types are string literals, so they can go into the SQL directly instead of through placeholders. Type the hook callbacks to what core passes them, which also lets the WP_Comment guards go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0126YH7wfjdvbPpuuNv1v92h
Summary
Closes #75144
Adds the foundation for emoji reactions on block notes. This PR is scoped to a small curated 5-emoji quick row (heart, celebration, smile, eyes, rocket). The full searchable emoji picker behind a
+button lives in a follow-up PR stacked on this one - per @t-hamano's request, so library/bundling decisions can be reviewed separately.Storage: reactions as a custom comment type
Reactions are stored as individual
reactioncomment records (one per user-emoji-note triple), as suggested by @swissspidy, in preference to JSON blobs in comment meta:This is more flexible, aligns with WordPress data patterns (similar to the React plugin), and could eventually extend to regular comments. The custom type is hidden from
wp-admin/edit-comments.phpand excluded from comment counts.Curated emoji set
The default 5 emojis (heart, celebration, smile, eyes, rocket) are stored as ASCII slugs (e.g.
heart). Storing slugs sidesteps utf8 vs utf8mb4 portability issues on the comments table and gives stable grouping in thereaction_summaryaggregation.The list is filterable via the
gutenberg_note_reaction_emojisPHP filter and exposed to clients through the REST API schema (reaction_emojisproperty onOPTIONS /wp/v2/comments), implementing the approach suggested by @Mamaduka.Efficient reaction loading
Each note response includes a
reaction_summaryfield -{ [emojiSlug]: { count, reacted, my_reaction_id } }. The schema lives inclass-gutenberg-rest-comment-controller-7-1.php, populated inprepare_item_for_response().get_items()pre-fetches summaries for the entire result set with one aggregatedGROUP BYquery plus one query for the current user's own reactions, regardless of how many notes are returned (seeprefetch_reaction_summaries()).reaction_summary- names are fetched on tooltip hover, so the initial notes load stays cheap.Popover architecture
The curated picker uses
@wordpress/components/Dropdown, whose Popover portals to<body>. This is essential because the collab sidebar has anoverflow: hiddenchain reaching all the way up to.interface-interface-skeleton__sidebar(framework-level); a non-portaled popover would clip. The note thread'sonBlurhandler exempts.components-popoverso that focus moving into the picker doesn't deselect the note (which would unmount the trigger and close the picker mid-click).Where the trigger lives
The 😊 trigger floats in the note's top corner, in the same column as Resolve and the actions menu, and stays faded until the note is hovered or the trigger itself is focused. A note nobody has reacted to costs no vertical space for the option, and the note text keeps a safe area on that side so it never runs underneath. Once a reaction lands the row drops into the flow, and the trigger trails the pills and wraps with them. This follows @annezazu's suggestion and @fcoveram's exploration, and was first built in #82313 on the follow-up PR before moving here per @draganescu's question.
tabIndex={ 0 }, so a thread-level:focus-withinwould hold for as long as the note is selected, and a thread-level:hoverwould reveal the trigger on every note in the thread at once.@media (hover: hover), so a touch device keeps a trigger it can actually see.Buttonvariants, no custom CSS.Related
Remaining feedback from #75549
useEntityRecordsfor reactions has noper_pagelimit. Consider lazy-loading reactions only when a note thread is opened. Comment@wordpress/icons(@ellatrix) - Low priority, fine for now. CommentAlready addressed
wordpress-7.1compat dir (@Mamaduka, @t-hamano) - Donereaction(@swissspidy) - Donegutenberg_note_reaction_emojisfilter, surfaced via REST schemaScreenshots
Screencast
Hover to reveal the trigger, react twice, then deselect the note:
Testing
For testing with multiple users, use the User Switching Plugin - https://wordpress.org/plugins/user-switching/
npm run test:e2e -- test/e2e/specs/editor/various/block-notes.spec.jsnpm run test:unit:php:base -- --filter=Gutenberg_REST_CommentsAI Use
Code and description both written with 🤖 Claude Code, with plenty of steering. I will review and test.