UNPKG

@wordpress/editor

Version:
246 lines (228 loc) 7.18 kB
import { useDispatch, useSelect } from '@wordpress/data'; import { useCallback } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { privateApis, type PostEditorAwarenessState, type PostSaveEvent, } from '@wordpress/core-data'; import { store as preferencesStore } from '@wordpress/preferences'; import { unlock } from '../../lock-unlock'; import { store as editorStore } from '../../store'; import { getCollaboratorDisplayName } from '../../utils/get-collaborator-display-name'; const { useOnCollaboratorJoin, useOnCollaboratorLeave, useOnPostSave } = unlock( privateApis ); /** * Notice IDs for each notification type. Using stable IDs prevents duplicate * notices if the same event is processed more than once. */ const NOTIFICATION_TYPE = { COLLAB_POST_UPDATED: 'collab-post-updated', COLLAB_USER_ENTERED: 'collab-user-entered', COLLAB_USER_EXITED: 'collab-user-exited', } as const; const PUBLISHED_STATUSES = [ 'publish', 'private', 'future' ]; /** * Returns the snackbar message for a post updated notification. * * @param name Display name of the collaborator who saved. * @param status WordPress post status at the time of save. * @param isFirstPublish Whether this save transitioned the post to published. */ function getPostUpdatedMessage( name: string, status: string, isFirstPublish: boolean ): string { if ( isFirstPublish ) { /* translators: %s: collaborator display name */ return sprintf( __( 'Post published by %s.' ), name ); } if ( PUBLISHED_STATUSES.includes( status ) ) { /* translators: %s: collaborator display name */ return sprintf( __( 'Post updated by %s.' ), name ); } /* translators: %s: collaborator display name */ return sprintf( __( 'Draft saved by %s.' ), name ); } /** * Hook that watches for collaborator join/leave events and remote save events, * dispatching snackbar notices accordingly. * * @param postId The ID of the post being edited. * @param postType The post type of the post being edited. */ export function useCollaboratorNotifications( postId: number | null, postType: string | null ): void { const { postStatus, isCollaborationEnabled, showJoinNotifications, showLeaveNotifications, showPostSaveNotifications, } = useSelect( ( select ) => { const { getCurrentPostAttribute, isCollaborationEnabledForCurrentPost, } = unlock( select( editorStore ) ); // Notification preferences default to enabled when unset. const getNotificationPreference = ( name: string ) => select( preferencesStore ).get( 'core', name ) ?? true; return { postStatus: getCurrentPostAttribute( 'status' ) as | string | undefined, isCollaborationEnabled: isCollaborationEnabledForCurrentPost(), showJoinNotifications: getNotificationPreference( 'showCollaborationJoinNotifications' ), showLeaveNotifications: getNotificationPreference( 'showCollaborationLeaveNotifications' ), showPostSaveNotifications: getNotificationPreference( 'showCollaborationPostSaveNotifications' ), }; }, [] ); const { createNotice } = useDispatch( noticesStore ); // Pass null when collaboration is disabled or a notification type is // turned off to prevent the hooks from subscribing to awareness state. const shouldShowJoinNotifications = isCollaborationEnabled && showJoinNotifications; const shouldShowLeaveNotifications = isCollaborationEnabled && showLeaveNotifications; const shouldShowPostSaveNotifications = isCollaborationEnabled && showPostSaveNotifications; // A disabled notification type passes null, which unsubscribes its hook; // callback guards handle any events already queued before then. const effectiveTarget = ( shouldShow: boolean ): [ number | null, string | null ] => shouldShow ? [ postId, postType ] : [ null, null ]; const [ joinPostId, joinPostType ] = effectiveTarget( shouldShowJoinNotifications ); const [ leavePostId, leavePostType ] = effectiveTarget( shouldShowLeaveNotifications ); const [ postSavePostId, postSavePostType ] = effectiveTarget( shouldShowPostSaveNotifications ); useOnCollaboratorJoin( joinPostId, joinPostType, useCallback( ( collaborator: PostEditorAwarenessState, me?: PostEditorAwarenessState ) => { if ( ! shouldShowJoinNotifications ) { return; } /* * Skip collaborators who were present before the current user * joined. Their enteredAt is earlier than ours, meaning we're * the newcomer. */ if ( me && collaborator.collaboratorInfo.enteredAt < me.collaboratorInfo.enteredAt ) { return; } void createNotice( 'info', sprintf( /* translators: %s: collaborator display name */ __( '%s has joined the post.' ), getCollaboratorDisplayName( collaborator.collaboratorInfo ) ), { id: `${ NOTIFICATION_TYPE.COLLAB_USER_ENTERED }-${ collaborator.collaboratorInfo.id ?? collaborator.clientId }`, type: 'snackbar', isDismissible: false, } ); }, [ createNotice, shouldShowJoinNotifications ] ) ); useOnCollaboratorLeave( leavePostId, leavePostType, useCallback( ( collaborator: PostEditorAwarenessState ) => { if ( ! shouldShowLeaveNotifications ) { return; } void createNotice( 'info', sprintf( /* translators: %s: collaborator display name */ __( '%s has left the post.' ), getCollaboratorDisplayName( collaborator.collaboratorInfo ) ), { id: `${ NOTIFICATION_TYPE.COLLAB_USER_EXITED }-${ collaborator.collaboratorInfo.id ?? collaborator.clientId }`, type: 'snackbar', isDismissible: false, } ); }, [ createNotice, shouldShowLeaveNotifications ] ) ); useOnPostSave( postSavePostId, postSavePostType, useCallback( ( saveEvent: PostSaveEvent, saver: PostEditorAwarenessState, prevEvent: PostSaveEvent | null ) => { if ( ! shouldShowPostSaveNotifications || ! postStatus ) { return; } // Prefer the remote status from Y.Doc (accurate at save time) // over the local Redux value, which may not have synced yet. const effectiveStatus = saveEvent.postStatus ?? postStatus ?? 'draft'; // Use the previous save event's status when available for // accurate first-publish detection across rapid saves. const prevStatus = prevEvent?.postStatus ?? postStatus; const isFirstPublish = ! ( prevStatus && PUBLISHED_STATUSES.includes( prevStatus ) ) && PUBLISHED_STATUSES.includes( effectiveStatus ); const message = getPostUpdatedMessage( getCollaboratorDisplayName( saver.collaboratorInfo ), effectiveStatus, isFirstPublish ); void createNotice( 'info', message, { id: `${ NOTIFICATION_TYPE.COLLAB_POST_UPDATED }-${ saver.collaboratorInfo.id ?? saver.clientId }`, type: 'snackbar', isDismissible: false, } ); }, [ createNotice, postStatus, shouldShowPostSaveNotifications ] ) ); }