Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions packages/edit-post/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export function initializeEditor(
enableChoosePatternModal: true,
isPublishSidebarEnabled: true,
showCollaborationCursor: false,
showCollaborationNotifications: true,
} );

if ( window.__clientSideMediaProcessing ) {
Expand Down
1 change: 1 addition & 0 deletions packages/edit-site/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export function initializeEditor( id, settings ) {
showListViewByDefault: false,
enableChoosePatternModal: true,
showCollaborationCursor: false,
showCollaborationNotifications: true,
} );

if ( window.__clientSideMediaProcessing ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ const mockCreateNotice = jest.fn();
let mockOnJoinCallback: Function | null = null;
let mockOnLeaveCallback: Function | null = null;
let mockOnPostSaveCallback: Function | null = null;
let lastJoinPostId: unknown;
let lastLeavePostId: unknown;
let lastSavePostId: unknown;
let mockEditorState = {
postStatus: 'draft',
isCollaborationEnabled: true,
showCollaborationNotifications: true,
};

jest.mock( '@wordpress/data', () => ( {
Expand All @@ -33,6 +37,10 @@ jest.mock( '@wordpress/notices', () => ( {
store: 'core/notices',
} ) );

jest.mock( '@wordpress/preferences', () => ( {
store: 'core/preferences',
} ) );

// Mock the editor store to prevent deep import chain (blocks, rich-text, etc.)
jest.mock( '../../../store', () => ( {
store: 'core/editor',
Expand All @@ -46,17 +54,20 @@ jest.mock( '@wordpress/core-data', () => ( {
jest.mock( '../../../lock-unlock', () => ( {
unlock: jest.fn( () => ( {
useOnCollaboratorJoin: jest.fn(
( _postId: unknown, _postType: unknown, callback: Function ) => {
( postId: unknown, _postType: unknown, callback: Function ) => {
lastJoinPostId = postId;
mockOnJoinCallback = callback;
}
),
useOnCollaboratorLeave: jest.fn(
( _postId: unknown, _postType: unknown, callback: Function ) => {
( postId: unknown, _postType: unknown, callback: Function ) => {
lastLeavePostId = postId;
mockOnLeaveCallback = callback;
}
),
useOnPostSave: jest.fn(
( _postId: unknown, _postType: unknown, callback: Function ) => {
( postId: unknown, _postType: unknown, callback: Function ) => {
lastSavePostId = postId;
mockOnPostSaveCallback = callback;
}
),
Expand Down Expand Up @@ -103,21 +114,40 @@ function makeMe( overrides: Record< string, unknown > = {} ) {
// --- Setup ---

function buildMockSelect() {
return () => ( {
getCurrentPostAttribute: ( attr: string ) =>
attr === 'status' ? mockEditorState.postStatus : undefined,
isCollaborationEnabledForCurrentPost: () =>
mockEditorState.isCollaborationEnabled,
} );
return ( storeKey: string ) => {
if ( storeKey === 'core/preferences' ) {
return {
get: ( scope: string, name: string ) => {
if (
scope === 'core' &&
name === 'showCollaborationNotifications'
) {
return mockEditorState.showCollaborationNotifications;
}
return undefined;
},
};
}
return {
getCurrentPostAttribute: ( attr: string ) =>
attr === 'status' ? mockEditorState.postStatus : undefined,
isCollaborationEnabledForCurrentPost: () =>
mockEditorState.isCollaborationEnabled,
};
};
}

beforeEach( () => {
mockOnJoinCallback = null;
mockOnLeaveCallback = null;
mockOnPostSaveCallback = null;
lastJoinPostId = undefined;
lastLeavePostId = undefined;
lastSavePostId = undefined;
mockEditorState = {
postStatus: 'draft',
isCollaborationEnabled: true,
showCollaborationNotifications: true,
};
mockCreateNotice.mockClear();
( useSelect as jest.Mock ).mockImplementation( ( selector: Function ) =>
Expand Down Expand Up @@ -337,4 +367,30 @@ describe( 'useCollaboratorNotifications', () => {
expect( mockCreateNotice ).not.toHaveBeenCalled();
} );
} );

describe( 'when notifications are disabled', () => {
it( 'passes null postId to hooks when showCollaborationNotifications preference is false', () => {
mockEditorState = {
...mockEditorState,
showCollaborationNotifications: false,
};
renderHook( () => useCollaboratorNotifications( 123, 'post' ) );

expect( lastJoinPostId ).toBeNull();
expect( lastLeavePostId ).toBeNull();
expect( lastSavePostId ).toBeNull();
} );

it( 'passes null postId to hooks when collaboration is disabled', () => {
mockEditorState = {
...mockEditorState,
isCollaborationEnabled: false,
};
renderHook( () => useCollaboratorNotifications( 123, 'post' ) );

expect( lastJoinPostId ).toBeNull();
expect( lastLeavePostId ).toBeNull();
expect( lastSavePostId ).toBeNull();
} );
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type PostEditorAwarenessState,
type PostSaveEvent,
} from '@wordpress/core-data';
import { store as preferencesStore } from '@wordpress/preferences';

/**
* Internal dependencies
Expand All @@ -30,16 +31,6 @@ const NOTIFICATION_TYPE = {
COLLAB_USER_EXITED: 'collab-user-exited',
} as const;

/**
* Development kill-switches for each notification type. Flip to `false`
* to suppress a category during local testing without removing code.
*/
const NOTIFICATIONS_CONFIG = {
userEntered: true,
userExited: true,
postUpdated: true,
};

const PUBLISHED_STATUSES = [ 'publish', 'private', 'future' ];

/**
Expand Down Expand Up @@ -77,23 +68,32 @@ export function useCollaboratorNotifications(
postId: number | null,
postType: string | null
): void {
const { postStatus, isCollaborationEnabled } = useSelect( ( select ) => {
const editorSel = select( editorStore );
return {
postStatus: editorSel.getCurrentPostAttribute( 'status' ) as
| string
| undefined,
isCollaborationEnabled:
editorSel.isCollaborationEnabledForCurrentPost(),
};
}, [] );
const { postStatus, isCollaborationEnabled, showNotifications } = useSelect(
( select ) => {
const editorSel = select( editorStore );
return {
postStatus: editorSel.getCurrentPostAttribute( 'status' ) as
| string
| undefined,
isCollaborationEnabled:
editorSel.isCollaborationEnabledForCurrentPost(),
showNotifications:
select( preferencesStore ).get(
'core',
'showCollaborationNotifications'
) ?? true,
};
},
[]
);

const { createNotice } = useDispatch( noticesStore );

// Pass null when collaboration is disabled to prevent the hooks
// from subscribing to awareness state.
const effectivePostId = isCollaborationEnabled ? postId : null;
const effectivePostType = isCollaborationEnabled ? postType : null;
// Pass null when collaboration is disabled or notifications are
// turned off to prevent the hooks from subscribing to awareness state.
const shouldSubscribe = isCollaborationEnabled && showNotifications;
const effectivePostId = shouldSubscribe ? postId : null;
const effectivePostType = shouldSubscribe ? postType : null;

useOnCollaboratorJoin(
effectivePostId,
Expand All @@ -103,10 +103,6 @@ export function useCollaboratorNotifications(
collaborator: PostEditorAwarenessState,
me?: PostEditorAwarenessState
) => {
if ( ! NOTIFICATIONS_CONFIG.userEntered ) {
return;
}

/*
* Skip collaborators who were present before the current user
* joined. Their enteredAt is earlier than ours, meaning we're
Expand Down Expand Up @@ -143,10 +139,6 @@ export function useCollaboratorNotifications(
effectivePostType,
useCallback(
( collaborator: PostEditorAwarenessState ) => {
if ( ! NOTIFICATIONS_CONFIG.userExited ) {
return;
}

void createNotice(
'info',
sprintf(
Expand Down Expand Up @@ -174,7 +166,7 @@ export function useCollaboratorNotifications(
saver: PostEditorAwarenessState,
prevEvent: PostSaveEvent | null
) => {
if ( ! NOTIFICATIONS_CONFIG.postUpdated || ! postStatus ) {
if ( ! postStatus ) {
return;
}

Expand Down
10 changes: 10 additions & 0 deletions packages/editor/src/components/preferences-modal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,16 @@ function PreferencesModalContents( { extraSections = {} } ) {
) }
label={ __( 'Show avatar in blocks' ) }
/>
<PreferenceToggleControl
scope="core"
featureName="showCollaborationNotifications"
help={ __(
'Show notifications when collaborators join, leave, or save the post.'
) }
label={ __(
'Show collaboration notifications'
) }
/>
</PreferencesModalSection>
<PreferencesModalSection
title={ __( 'Document settings' ) }
Expand Down
Loading