Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e032fa1
Suggestions: Register inline RichText format types for proposed adds …
adamsilverstein May 1, 2026
b52f596
Suggestions: Add markContentDiff and stripSuggestionMarks helpers
adamsilverstein May 1, 2026
6f98b1e
Suggestions: Render proposed text changes inline via overlay HOC
adamsilverstein May 1, 2026
1da0cf2
Suggestions: Add e2e coverage for inline diff marks in the editor canvas
adamsilverstein May 1, 2026
45fec27
Suggestions: Tighten inline docs across the suggest-mode overlay path
adamsilverstein May 4, 2026
905841f
Suggestions: Move inline-diff styles into the block-editor content bu…
adamsilverstein May 6, 2026
ef72cfd
Suggestions: Carry suggester avatar color on inline diff marks
adamsilverstein May 6, 2026
b67ed11
Suggestions: Tint inline diff marks with the suggester's avatar color
adamsilverstein May 6, 2026
42565cf
Merge branch 'add-suggestion-mode' into try/suggest-mode-inline-formats
adamsilverstein May 6, 2026
09908ca
RTC: Fix compaction unit test (#77986)
alecgeatches May 6, 2026
5343fb1
RTC: Fix divergence when two offline users reconnect (#77980)
alecgeatches May 5, 2026
6a423ce
Suggestions: Drop redundant bare import of inline-formats
adamsilverstein May 12, 2026
ed7110c
Suggestions: Coalesce consecutive diff segments into one wrapper
adamsilverstein May 12, 2026
2ce6837
Suggestions: Show inline diff marks once edits settle, not only on blur
adamsilverstein May 12, 2026
5e0a9f6
Suggestions: Hydrate overlay from persisted suggestion comments
adamsilverstein May 12, 2026
d76aa7f
Suggestions: Skip hydrator re-seed when persisted payload is unchanged
adamsilverstein May 12, 2026
8baa32d
Suggestions: Don't mask reviewer writes when overlay is hydrated
adamsilverstein May 12, 2026
3f6c37d
Suggestions: Drop vertical pipe shadows from inline addition runs
adamsilverstein May 13, 2026
efe0e23
Suggestions: Suppress inline diff marks while the block is selected
adamsilverstein May 13, 2026
445a59f
Suggestions: Let overlay tests register the block-editor store
adamsilverstein May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backport-changelog/7.0/11716.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://github.com/WordPress/wordpress-develop/pull/11716

* https://github.com/WordPress/gutenberg/pull/77980
11 changes: 8 additions & 3 deletions lib/compat/wordpress-7.0/class-wp-http-polling-sync-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -500,9 +500,14 @@ private function process_sync_update( string $room, int $client_id, int $cursor,
return $this->add_update( $room, $client_id, $type, $data );
}

// Reaching this point means there's a newer compaction, so we can
// silently ignore this one.
return true;
/*
* A newer compaction already advanced the cursor, but we
* can not safely drop an update. The incoming bytes still encode
* operations other clients may not have seen, so store them as a
* regular update. Y.applyUpdateV2 merges state-as-update blobs
* idempotently, so overlap with the existing compaction is safe.
*/
return $this->add_update( $room, $client_id, self::UPDATE_TYPE_UPDATE, $data );

case self::UPDATE_TYPE_SYNC_STEP1:
case self::UPDATE_TYPE_SYNC_STEP2:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@use "@wordpress/base-styles/colors" as *;
@use "@wordpress/base-styles/variables" as *;

// Visual treatment for RichText runs marked as a pending suggestion. The
// `.has-suggestion-deletion` / `.has-suggestion-addition` classes are
// applied programmatically by the editor-package overlay HOC; this
// stylesheet only knows about the resulting class names. Lives in
// `block-editor` because the canvas iframe loads `wp-block-editor-content`
// (compiled from this package) but does NOT load the editor-package
// chrome stylesheets — keeping these rules here is what makes them apply
// inside the iframe.

// Default suggestion colors, used when the suggester's avatar color
// can't be resolved (e.g., anonymous edits or pre-collab sessions).
// `markContentDiff` writes the suggester's avatar color into
// `--suggestion-author-color` on each <del>/<ins> run, so individual
// suggestions tint to their author's color the same way live cursors do
// — Google Docs-style. Rules below consume the variable with the
// red/green pair as the fallback.
$suggestion-deletion-color: #cc1818;
$suggestion-addition-color: #007017;
$suggestion-author-color: var(--suggestion-author-color, #{$suggestion-addition-color});

.has-suggestion-deletion {
color: var(--suggestion-author-color, #{$suggestion-deletion-color});
text-decoration: line-through;
text-decoration-color: var(--suggestion-author-color, #{$suggestion-deletion-color});
}

.has-suggestion-addition {
color: $suggestion-author-color;
text-decoration: underline;
text-decoration-color: $suggestion-author-color;
}
1 change: 1 addition & 0 deletions packages/block-editor/src/content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@use "@wordpress/base-styles/mixins" as *;
@use "./components/block-icon/content.scss" as *;
@use "./components/block-list/content.scss" as *;
@use "./components/block-list/content-suggestion.scss" as *;
@use "./components/block-list-appender/content.scss" as *;
@use "./components/block-content-overlay/content.scss" as *;
@use "./components/block-draggable/content.scss" as *;
Expand Down
2 changes: 2 additions & 0 deletions packages/editor/src/components/provider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
SuggestionOverlayProvider,
SuggestionAutoSave,
SuggestionStoreInterceptor,
SuggestionOverlayHydrator,
registerSuggestionOverlayFilter,
} from '../suggestion-mode';

Expand Down Expand Up @@ -480,6 +481,7 @@ export const ExperimentalEditorProvider = withRegistryProvider(
<PatternDuplicateModal />
<SuggestionStoreInterceptor />
<SuggestionAutoSave />
<SuggestionOverlayHydrator />
{ window?.__experimentalMediaEditorModal && (
<MediaEditorModalMount />
) }
Expand Down
186 changes: 186 additions & 0 deletions packages/editor/src/components/suggestion-mode/hydrator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/**
* Seeds the suggestion overlay from persisted `_wp_suggestion` comment
* payloads on editor mount and whenever the comment list updates. Two
* scenarios depend on this:
*
* - A suggester reloads the page after auto-save fired. The in-memory
* overlay (see `overlay-context.js`) starts empty, but the persisted
* note carries the proposed values — without re-seeding, the inline
* diff marks disappear until the suggestion is accepted or rejected.
* - A reviewer (post author, admin) opens the same post in any intent.
* They never wrote the suggestion themselves, so their local overlay is
* empty too; without seeding, they only see the sidebar summary, never
* the inline strike-through/insertion on the canvas.
*
* The hydrator reuses the existing `useNoteThreads` hook from the collab
* sidebar so the entity-records query (and its cache) is shared — no extra
* REST traffic, no risk of the two consumers diverging on what counts as a
* note thread.
*
* Live editing wins over hydration: if an entry already exists for a block
* and was *not* sourced from the hydrator, the seed is skipped. That way a
* suggester typing into a block whose previous suggestion was persisted
* doesn't have their unsaved overlay clobbered by a refresh of the comment
* list.
*/

/**
* WordPress dependencies
*/
import { useEffect } from '@wordpress/element';
import { useSelect } from '@wordpress/data';

/**
* Internal dependencies
*/
import { useSuggestionOverlay } from './overlay-context';
import { parseSuggestionPayload } from './provider';
import { useNoteThreads } from '../collab-sidebar/hooks';
import { store as editorStore } from '../../store';

/**
* Shallow equality over two flat objects of primitive-ish values. The
* hydrator uses this to avoid re-dispatching `SEED_FROM_COMMENT` on every
* render when the persisted payload hasn't actually moved — the reducer
* would otherwise mint a new state reference each time and the effect would
* loop. Strings, numbers, booleans, null, and undefined compare by value;
* non-primitive values (object attribute payloads like `style`) fall back to
* reference equality. That matches the suggestion payload shape today —
* `attribute-set` ops carry primitive or string-serialized values.
*
* @param {Object} a First object.
* @param {Object} b Second object.
* @return {boolean} True when both objects have the same keys and matching values.
*/
function shallowEqual( a, b ) {
if ( a === b ) {
return true;
}
if ( ! a || ! b ) {
return false;
}
const ak = Object.keys( a );
const bk = Object.keys( b );
if ( ak.length !== bk.length ) {
return false;
}
for ( const k of ak ) {
if ( a[ k ] !== b[ k ] ) {
return false;
}
}
return true;
}

/**
* Derive baseline + overlay attribute pairs from a parsed payload's
* `attribute-set` operations. Structural ops (`block-remove`,
* `block-insert-after`, `block-move`) are already rendered via the block's
* own `metadata.suggestion` marker on the live canvas, so the hydrator
* skips them — including them here would re-do work that the structural
* BlockListBlock filter already handles.
*
* @param {{ operations: Array<{type: string, attribute?: string, before?: *, after?: *}> }|null} payload
* @return {{ baselineAttributes: Object, overlayAttributes: Object }|null} Pair
* suitable for `SEED_FROM_COMMENT`, or null when the payload carries no
* attribute-set ops.
*/
function attributePairsFromPayload( payload ) {
if ( ! payload || ! Array.isArray( payload.operations ) ) {
return null;
}
const baselineAttributes = {};
const overlayAttributes = {};
let count = 0;
for ( const op of payload.operations ) {
if (
op?.type !== 'attribute-set' ||
typeof op.attribute !== 'string'
) {
continue;
}
baselineAttributes[ op.attribute ] = op.before;
overlayAttributes[ op.attribute ] = op.after;
count++;
}
if ( count === 0 ) {
return null;
}
return { baselineAttributes, overlayAttributes };
}

/**
* Mounted once inside `SuggestionOverlayProvider`. Watches the post's note
* threads and seeds an overlay entry for any unresolved (`status: 'hold'`)
* suggestion whose payload carries an `attribute-set` operation. Re-runs
* whenever the thread list or block tree changes so a newly-loaded
* suggestion (or a hot-reloaded block tree) ends up reflected.
*/
export default function SuggestionOverlayHydrator() {
const postId = useSelect(
( select ) => select( editorStore ).getCurrentPostId(),
[]
);
const { unresolvedNotes } = useNoteThreads( postId );
const { entries, seedFromComment } = useSuggestionOverlay();

useEffect( () => {
if ( ! unresolvedNotes || unresolvedNotes.length === 0 ) {
return;
}
for ( const thread of unresolvedNotes ) {
const clientId = thread.blockClientId;
if ( ! clientId ) {
continue;
}
const payload = parseSuggestionPayload(
thread.meta?._wp_suggestion
);
if ( ! payload ) {
continue;
}
const pairs = attributePairsFromPayload( payload );
if ( ! pairs ) {
continue;
}
const existing = entries[ clientId ];
// Don't clobber a live overlay that wasn't itself sourced from
// the hydrator — the suggester may be mid-edit and the in-memory
// state is more current than the persisted comment.
if (
existing &&
existing.hydratedFromCommentId !== thread.id &&
Object.keys( existing.overlayAttributes ?? {} ).length > 0
) {
continue;
}
// Already hydrated from this comment and the persisted values
// haven't changed — no-op rather than re-dispatching. The reducer
// would otherwise produce a new state reference on every render,
// looping the effect indefinitely.
if (
existing &&
existing.hydratedFromCommentId === thread.id &&
shallowEqual(
existing.baselineAttributes,
pairs.baselineAttributes
) &&
shallowEqual(
existing.overlayAttributes,
pairs.overlayAttributes
)
) {
continue;
}
seedFromComment(
clientId,
payload.blockName ?? null,
thread.id,
pairs.baselineAttributes,
pairs.overlayAttributes
);
}
}, [ unresolvedNotes, entries, seedFromComment ] );

return null;
}
6 changes: 6 additions & 0 deletions packages/editor/src/components/suggestion-mode/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export {
SUGGESTED_DELETION_FORMAT,
SUGGESTED_ADDITION_FORMAT,
registerSuggestionFormats,
} from './inline-formats';
export {
SuggestionOverlayProvider,
useSuggestionOverlay,
Expand All @@ -9,6 +14,7 @@ export {
} from './with-suggestion-overlay';
export { default as SuggestionAutoSave } from './auto-save';
export { default as SuggestionStoreInterceptor } from './store-interceptor';
export { default as SuggestionOverlayHydrator } from './hydrator';
export {
useSuggestionsProvider,
operationsFromOverlay,
Expand Down
Loading
Loading