Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
683e148
Real-time collaboration: [pr-72373][merged-trunk] Implement CRDT pers…
chriszarate Oct 27, 2025
213cd9e
Real-time collaboration: [pr-72332][merged-trunk] Add support for syn…
chriszarate Oct 27, 2025
a2c766b
Real-time collaboration: [pr-72407][merged-trunk] Add UndoManager sup…
chriszarate Oct 16, 2025
0d074ba
Real-time collaboration: [pr-72437] Add Yjs awareness
chriszarate Oct 16, 2025
04cf180
Real-time collaboration: [pr-72326] Allow post-locked-modal to be ove…
pkevan Oct 14, 2025
8655bd3
Real-time collaboration: [pr-72405] Fix "Show Template" view
alecgeatches Sep 15, 2025
a1f1e7e
Real-time collaboration: [pr-73526][merged-trunk] Skip syncing for "s…
chriszarate Dec 1, 2025
48b98f9
Real-time collaboration: [no-pr] Remove @wordpress/sync from bundled …
chriszarate Oct 1, 2025
7556b51
Real-time collaboration: [no-pr] Add <EditorsPresence> and <BlockCanv…
chriszarate Oct 17, 2025
8b750aa
Real-time collaboration: [no-pr] Replace fast-diff in quill-delta (wi…
alecgeatches Oct 23, 2025
cfdd8e0
Real-time collaboration: [no-pr] Check for the current attribute exis…
ingeniumed Oct 23, 2025
f4ed11b
Real-time collaboration: [no-pr] Ensure block attribute is Y.Text
chriszarate Oct 27, 2025
ae2b0be
Real-time collaboration: [no-pr] Don't sync core/freeform block witho…
chriszarate Oct 27, 2025
d7aeb68
Real-time collaboration: [no-pr] Improve type safety with YMapWrap
chriszarate Oct 31, 2025
007601d
Real-time collaboration: [no-pr] Fix persisted document meta and add …
chriszarate Oct 31, 2025
fb0c2d3
Real-time collaboration: [no-pr] Refetch entity when it is saved by a…
chriszarate Oct 31, 2025
28416ed
Real-time collaboration: [no-pr] Sync collections
chriszarate Nov 7, 2025
af1d8c1
Real-time collaboration: [no-pr] Add CollaborationMode SlotFill for e…
shekharnwagh Nov 14, 2025
c1ba36d
Real-time collaboration: [no-pr] Do not call saveRecord when persiste…
chriszarate Nov 20, 2025
f0653fc
Real-time collaboration: [no-pr] Temp fix?: Ignore type error
chriszarate Nov 25, 2025
4598009
Real-time collaboration: Add collaborator modes (#73474)
ingeniumed Dec 7, 2025
723d51a
Add reactive enforced mode for code editor
ingeniumed Dec 9, 2025
a566a72
Get the whole reset flow working
ingeniumed Dec 9, 2025
6625b02
Quick cleanup
ingeniumed Dec 9, 2025
ccc0e36
Tweak the approach to use meta instead
ingeniumed Dec 10, 2025
cf5ec67
Remove the unnecessary two post properties
ingeniumed Dec 10, 2025
cc3aef0
Move the collaborator mode to core-data
ingeniumed Dec 11, 2025
c01a17b
Fix the prettier issue
ingeniumed Dec 11, 2025
584fb3d
Fix the imports issue
ingeniumed Dec 11, 2025
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
24 changes: 24 additions & 0 deletions docs/reference-guides/data/data-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,18 @@ _Returns_

- `Array< any >`: Block pattern list.

### getCollaboratorMode

Returns the current collaborator mode.

_Parameters_

- _state_ `Object`: Data state.

_Returns_

- `CollaboratorMode`: Collaborator mode.

### getCurrentTheme

Return the current theme.
Expand Down Expand Up @@ -923,6 +935,18 @@ _Parameters_
- _options.\_\_unstableFetch_ `[Function]`: Internal use only. Function to call instead of `apiFetch()`. Must return a promise.
- _options.throwOnError_ `[boolean]`: If false, this action suppresses all the exceptions. Defaults to false.

### setCollaboratorMode

Returns an action object used in signalling that the collaborator mode has been set.

_Parameters_

- _collaboratorMode_ `'view' | 'edit'`: The collaborator mode.

_Returns_

- `Object`: Action object.

### undo

Action triggered to undo the last edit to an entity record, if any.
Expand Down
36 changes: 36 additions & 0 deletions lib/experimental/synchronization.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,39 @@ function gutenberg_rest_api_init_collaborative_editing() {
wp_add_inline_script( 'wp-sync', 'window.__experimentalCollaborativeEditingSecret = "' . $collaborative_editing_secret . '";', 'before' );
}
add_action( 'admin_init', 'gutenberg_rest_api_init_collaborative_editing' );

/**
* Registers post meta for persisting CRDT documents.
*/
function gutenberg_rest_api_crdt_post_meta() {
$gutenberg_experiments = get_option( 'gutenberg-experiments' );
if ( ! $gutenberg_experiments || ! array_key_exists( 'gutenberg-sync-collaboration', $gutenberg_experiments ) ) {
return;
}

// This string must match WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE in @wordpress/sync.
$persisted_crdt_post_meta_key = '_crdt_document';

register_meta(
'post',
$persisted_crdt_post_meta_key,
array(
'auth_callback' => function ( bool $_allowed, string $_meta_key, int $object_id, int $user_id ): bool {
return user_can( $user_id, 'edit_post', $object_id );
},
// IMPORTANT: Revisions must be disabled because we always want to preserve
// the latest persisted CRDT document, even when a revision is restored.
// This ensures that we can continue to apply updates to a shared document
// and peers can simply merge the restored revision like any other incoming
// update.
//
// If we want to persist CRDT documents alongisde revisions in the
// future, we should do so in a separate meta key.
'revisions_enabled' => false,
'show_in_rest' => true,
'single' => true,
'type' => 'string',
)
);
}
add_action( 'init', 'gutenberg_rest_api_crdt_post_meta' );
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/block-editor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ _Returns_

- `Element`: Block Breadcrumb.

### BlockCanvasCover

Undocumented declaration.

### BlockColorsStyleSelector

Undocumented declaration.
Expand Down
44 changes: 44 additions & 0 deletions packages/block-editor/src/components/block-canvas/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { useMergeRefs, useViewportMatch } from '@wordpress/compose';
import { useRef } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { createSlotFill } from '@wordpress/components';

/**
* Internal dependencies
Expand All @@ -19,6 +20,8 @@ import { useBlockCommands } from '../use-block-commands';
import { store as blockEditorStore } from '../../store';
import { unlock } from '../../lock-unlock';

export const BlockCanvasCover = createSlotFill( 'BlockCanvasCover' );

// EditorStyles is a memoized component, so avoid passing a new
// object reference on each render.
const EDITOR_STYLE_TRANSFORM_OPTIONS = {
Expand Down Expand Up @@ -74,6 +77,27 @@ export function ExperimentalBlockCanvas( {
>
{ children }
</WritingFlow>

<BlockCanvasCover.Slot fillProps={ { containerRef: localRef } }>
{ ( covers ) =>
covers.map( ( cover, index ) => (
<div
key={ index }
className="block-canvas-cover"
style={ {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
} }
>
{ cover }
</div>
) )
}
</BlockCanvasCover.Slot>
</BlockTools>
);
}
Expand All @@ -95,6 +119,26 @@ export function ExperimentalBlockCanvas( {
>
<EditorStyles styles={ styles } />
{ children }
<BlockCanvasCover.Slot fillProps={ { containerRef: localRef } }>
{ ( covers ) =>
covers.map( ( cover, index ) => (
<div
key={ index }
className="block-canvas-cover"
style={ {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
} }
>
{ cover }
</div>
) )
}
</BlockCanvasCover.Slot>
</Iframe>
</BlockTools>
);
Expand Down
16 changes: 13 additions & 3 deletions packages/block-editor/src/components/provider/use-block-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,25 @@ export default function useBlockSync( {
// and so it would already be persisted.
__unstableMarkNextChangeAsNotPersistent();
if ( clientId ) {
const blockName = getBlockName( clientId );
const isPostContentBlock = blockName === 'core/post-content';

// It is important to batch here because otherwise,
// as soon as `setHasControlledInnerBlocks` is called
// the effect to restore might be triggered
// before the actual blocks get set properly in state.
registry.batch( () => {
setHasControlledInnerBlocks( clientId, true );
const storeBlocks = controlledBlocks.map( ( block ) =>
cloneBlock( block )
);

// For post-content block children, preserve the
// original blocks to maintain UUIDs used for
// multi-user collaboration
//
// Unsure: Why are these blocks being cloned? Do they need to be?
const storeBlocks = isPostContentBlock
? controlledBlocks
: controlledBlocks.map( ( block ) => cloneBlock( block ) );

if ( subscribedRef.current ) {
pendingChangesRef.current.incoming = storeBlocks;
}
Expand Down
1 change: 1 addition & 0 deletions packages/block-editor/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export * from './utils';
export { storeConfig, store } from './store';
export { SETTINGS_DEFAULTS } from './store/defaults';
export { privateApis } from './private-apis';
export { BlockCanvasCover } from './components/block-canvas';
24 changes: 24 additions & 0 deletions packages/core-data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,18 @@ _Parameters_
- _options.\_\_unstableFetch_ `[Function]`: Internal use only. Function to call instead of `apiFetch()`. Must return a promise.
- _options.throwOnError_ `[boolean]`: If false, this action suppresses all the exceptions. Defaults to false.

### setCollaboratorMode

Returns an action object used in signalling that the collaborator mode has been set.

_Parameters_

- _collaboratorMode_ `'view' | 'edit'`: The collaborator mode.

_Returns_

- `Object`: Action object.

### undo

Action triggered to undo the last edit to an entity record, if any.
Expand Down Expand Up @@ -435,6 +447,18 @@ _Returns_

- `Array< any >`: Block pattern list.

### getCollaboratorMode

Returns the current collaborator mode.

_Parameters_

- _state_ `Object`: Data state.

_Returns_

- `CollaboratorMode`: Collaborator mode.

### getCurrentTheme

Return the current theme.
Expand Down
40 changes: 40 additions & 0 deletions packages/core-data/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ import { STORE_NAME } from './name';
import { LOCAL_EDITOR_ORIGIN, getSyncManager } from './sync';
import logEntityDeprecation from './utils/log-entity-deprecation';

/**
* Returns an action object used in signalling that the collaborator mode has been set.
*
* @param {'view' | 'edit'} collaboratorMode The collaborator mode.
*
* @return {Object} Action object.
*/
export function setCollaboratorMode( collaboratorMode ) {
return {
type: 'SET_COLLABORATOR_MODE',
collaboratorMode,
};
}

/**
* Returns an action object used in signalling that authors have been received.
* Ignored from documentation as it's internal to the data store.
Expand Down Expand Up @@ -326,6 +340,18 @@ export const deleteEntityRecord =
} );

await dispatch( removeItems( kind, name, recordId, true ) );

if (
window.__experimentalEnableSync &&
entityConfig.syncConfig
) {
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
const objectType = `${ kind }/${ name }`;
const objectId = recordId;

getSyncManager()?.unload( objectType, objectId );
}
}
} catch ( _error ) {
hasError = true;
error = _error;
Expand Down Expand Up @@ -689,6 +715,20 @@ export const saveEntityRecord =
true,
edits
);
if (
window.__experimentalEnableSync &&
entityConfig.syncConfig
) {
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
getSyncManager()?.update(
`${ kind }/${ name }`,
recordId,
updatedRecord,
LOCAL_EDITOR_ORIGIN,
true // isSave
);
}
}
}
} catch ( _error ) {
hasError = true;
Expand Down
Loading
Loading