From 45f43e62772f52b0894f13226b0c06ec29c3c118 Mon Sep 17 00:00:00 2001 From: Max Schmeling Date: Wed, 28 Jan 2026 16:27:46 -0600 Subject: [PATCH 01/14] Add UI components and hook up to real awareness data --- packages/core-data/src/private-apis.js | 2 + .../collaborators-presence/avatar.tsx | 55 +++++++++ .../collaborators-presence/index.tsx | 107 +++++++++++++++++ .../collaborators-presence/list.tsx | 109 ++++++++++++++++++ .../collaborators-presence/styles/avatar.scss | 28 +++++ .../styles/collaborators-list.scss | 97 ++++++++++++++++ .../styles/collaborators-presence.scss | 59 ++++++++++ .../editor/src/components/header/index.js | 16 ++- packages/sync/src/providers/index.ts | 3 +- 9 files changed, 474 insertions(+), 2 deletions(-) create mode 100644 packages/editor/src/components/collaborators-presence/avatar.tsx create mode 100644 packages/editor/src/components/collaborators-presence/index.tsx create mode 100644 packages/editor/src/components/collaborators-presence/list.tsx create mode 100644 packages/editor/src/components/collaborators-presence/styles/avatar.scss create mode 100644 packages/editor/src/components/collaborators-presence/styles/collaborators-list.scss create mode 100644 packages/editor/src/components/collaborators-presence/styles/collaborators-presence.scss diff --git a/packages/core-data/src/private-apis.js b/packages/core-data/src/private-apis.js index e9fcf36f7e6090..8c963572435bfc 100644 --- a/packages/core-data/src/private-apis.js +++ b/packages/core-data/src/private-apis.js @@ -3,10 +3,12 @@ */ import { useEntityRecordsWithPermissions } from './hooks/use-entity-records'; import { RECEIVE_INTERMEDIATE_RESULTS } from './utils'; +import { useActiveUsers } from './hooks/use-post-editor-awareness-state'; import { lock } from './lock-unlock'; export const privateApis = {}; lock( privateApis, { useEntityRecordsWithPermissions, RECEIVE_INTERMEDIATE_RESULTS, + useActiveUsers, } ); diff --git a/packages/editor/src/components/collaborators-presence/avatar.tsx b/packages/editor/src/components/collaborators-presence/avatar.tsx new file mode 100644 index 00000000000000..4c8519351c7cc4 --- /dev/null +++ b/packages/editor/src/components/collaborators-presence/avatar.tsx @@ -0,0 +1,55 @@ +import './styles/avatar.scss'; + +type AvatarSize = 'small' | 'medium'; + +/** + * Renders a circular avatar bubble for a user with an optional border. + * @param root0 + * @param root0.userInfo + * @param root0.userInfo.name + * @param root0.userInfo.color + * @param root0.userInfo.avatar_urls + * @param root0.showUserColorBorder + * @param root0.size + */ +export function Avatar( { + userInfo, + showUserColorBorder, + size = 'small', +}: { + userInfo: { + name: string; + color: string; + avatar_urls?: Record< string, string >; + }; + showUserColorBorder?: boolean; + size?: AvatarSize; +} ) { + const className = [ + 'vip-real-time-collaboration-avatar', + `vip-real-time-collaboration-avatar--${ size }`, + showUserColorBorder && + 'vip-real-time-collaboration-avatar--with-color-border', + ] + .filter( Boolean ) + .join( ' ' ); + + const avatarUrl = + userInfo.avatar_urls?.[ 48 ] || + userInfo.avatar_urls?.[ 96 ] || + userInfo.avatar_urls?.[ 24 ]; + + const avatarStyles: React.CSSProperties & + Record< `--${ string }`, string > = { + '--avatar-url': `url(${ avatarUrl })`, + '--user-color': userInfo.color, + }; + + return ( + -
+
{ activeUsers.map( ( userState ) => (
+} ) => ( + setIsPopoverVisible( false ) } + > +
+
+
+ Collaborators + { activeUsers.length }
-
- { activeUsers.map( ( userState ) => ( - - ) ) } +
+
- - ); -} +
+ { activeUsers.map( ( userState ) => ( + + ) ) } +
+
+ +); From ffecfef251a7a7453f003f96423df5b0042f0456 Mon Sep 17 00:00:00 2001 From: Max Schmeling Date: Fri, 30 Jan 2026 12:10:14 -0600 Subject: [PATCH 07/14] Removed some duplicate types and some code related to the cursor registry --- .../collaborators-presence/avatar.tsx | 10 ++------ .../collaborators-presence/index.tsx | 24 +++++++------------ .../collaborators-presence/list.tsx | 16 ++++--------- 3 files changed, 14 insertions(+), 36 deletions(-) diff --git a/packages/editor/src/components/collaborators-presence/avatar.tsx b/packages/editor/src/components/collaborators-presence/avatar.tsx index 12a85aa9812ce7..c4c26497360919 100644 --- a/packages/editor/src/components/collaborators-presence/avatar.tsx +++ b/packages/editor/src/components/collaborators-presence/avatar.tsx @@ -1,3 +1,4 @@ +import { type PostEditorAwarenessState } from '../../../../core-data/src/awareness/types'; import './styles/avatar.scss'; type AvatarSize = 'small' | 'medium'; @@ -6,9 +7,6 @@ type AvatarSize = 'small' | 'medium'; * Renders a circular avatar bubble for a user with an optional border. * @param root0 * @param root0.userInfo - * @param root0.userInfo.name - * @param root0.userInfo.color - * @param root0.userInfo.avatar_urls * @param root0.showUserColorBorder * @param root0.size */ @@ -17,11 +15,7 @@ export function Avatar( { showUserColorBorder, size = 'small', }: { - userInfo: { - name: string; - color: string; - avatar_urls?: Record< string, string >; - }; + userInfo: PostEditorAwarenessState[ 'userInfo' ]; showUserColorBorder?: boolean; size?: AvatarSize; } ) { diff --git a/packages/editor/src/components/collaborators-presence/index.tsx b/packages/editor/src/components/collaborators-presence/index.tsx index 047b8c3e629025..311dc132bc8274 100644 --- a/packages/editor/src/components/collaborators-presence/index.tsx +++ b/packages/editor/src/components/collaborators-presence/index.tsx @@ -5,23 +5,12 @@ import { privateApis } from '@wordpress/core-data'; import { Avatar } from './avatar'; import { CollaboratorsList } from './list'; import { unlock } from '../../lock-unlock'; +import { type PostEditorAwarenessState } from '../../../../core-data/src/awareness/types'; import './styles/collaborators-presence.scss'; const { useActiveUsers } = unlock( privateApis ); -interface UserInfo { - name: string; - color: string; - avatar_urls?: Record< string, string >; -} - -interface UserState { - clientId: string; - isMe: boolean; - userInfo: UserInfo; -} - interface AvatarsProps { postId: number | null; postType: string | null; @@ -30,12 +19,15 @@ interface AvatarsProps { /** * Renders a list of avatars for the active users, with a maximum of 3 visible avatars. * Shows a popover with all users on hover. - * @param root0 - * @param root0.postId - * @param root0.postType + * @param root0 Component props + * @param root0.postId The ID of the post + * @param root0.postType The type of the post */ export function CollaboratorsPresence( { postId, postType }: AvatarsProps ) { - const activeUsers = useActiveUsers( postId, postType ) as UserState[]; + const activeUsers = useActiveUsers( + postId, + postType + ) as PostEditorAwarenessState[]; // Filter out current user - we never show ourselves in the list const otherActiveUsers = activeUsers.filter( ( user ) => ! user.isMe ); diff --git a/packages/editor/src/components/collaborators-presence/list.tsx b/packages/editor/src/components/collaborators-presence/list.tsx index 4e29b1a1586211..c0af3c08fe7b09 100644 --- a/packages/editor/src/components/collaborators-presence/list.tsx +++ b/packages/editor/src/components/collaborators-presence/list.tsx @@ -4,14 +4,10 @@ import { close } from '@wordpress/icons'; import { Avatar } from './avatar'; import './styles/collaborators-list.scss'; -import { type UserInfo } from '../../../../core-data/src/awareness/types'; +import { type PostEditorAwarenessState } from '../../../../core-data/src/awareness/types'; interface CollaboratorsListProps { - activeUsers: { - clientId: string; - isConnected: boolean; - userInfo: UserInfo; - }[]; + activeUsers: PostEditorAwarenessState[]; popoverAnchor?: HTMLElement | null; setIsPopoverVisible: ( isVisible: boolean ) => void; } @@ -57,12 +53,8 @@ export const CollaboratorsList: React.FC< CollaboratorsListProps > = ( { { isPopoverVisible && ( ( { - clientId: user.clientId, - isConnected: true, - userInfo: user.userInfo, - } ) ) } + activeUsers={ otherActiveUsers } popoverAnchor={ popoverAnchor } setIsPopoverVisible={ setIsPopoverVisible } /> From ccb5a80aebd3938212bcabb80c83f338889b59ee Mon Sep 17 00:00:00 2001 From: Max Schmeling Date: Fri, 30 Jan 2026 12:20:35 -0600 Subject: [PATCH 09/14] Extract AvatarProps interface for --- .../collaborators-presence/avatar.tsx | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/editor/src/components/collaborators-presence/avatar.tsx b/packages/editor/src/components/collaborators-presence/avatar.tsx index c4c26497360919..69f33a57e0931e 100644 --- a/packages/editor/src/components/collaborators-presence/avatar.tsx +++ b/packages/editor/src/components/collaborators-presence/avatar.tsx @@ -3,22 +3,25 @@ import './styles/avatar.scss'; type AvatarSize = 'small' | 'medium'; +interface AvatarProps { + userInfo: PostEditorAwarenessState[ 'userInfo' ]; + showUserColorBorder?: boolean; + size?: AvatarSize; +} + /** * Renders a circular avatar bubble for a user with an optional border. - * @param root0 - * @param root0.userInfo - * @param root0.showUserColorBorder - * @param root0.size + * + * @param {Object} props Component props. + * @param {Object} props.userInfo User information. + * @param {boolean} props.showUserColorBorder Whether to show the user color border. + * @param {string} props.size Size of the avatar. */ -export function Avatar( { +export const Avatar: React.FC< AvatarProps > = ( { userInfo, showUserColorBorder, size = 'small', -}: { - userInfo: PostEditorAwarenessState[ 'userInfo' ]; - showUserColorBorder?: boolean; - size?: AvatarSize; -} ) { +} ) => { const className = [ 'editor-collaborators-presence__avatar', `editor-collaborators-presence__avatar--${ size }`, @@ -46,4 +49,4 @@ export function Avatar( { aria-hidden="true" /> ); -} +}; From 2f44103544f440c499d3244cadbd6c701865b573 Mon Sep 17 00:00:00 2001 From: Max Schmeling Date: Fri, 30 Jan 2026 12:25:33 -0600 Subject: [PATCH 10/14] Uses clsx in and updates CSSProperties type usage --- .../components/collaborators-presence/avatar.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/editor/src/components/collaborators-presence/avatar.tsx b/packages/editor/src/components/collaborators-presence/avatar.tsx index 69f33a57e0931e..57eb19f30f6922 100644 --- a/packages/editor/src/components/collaborators-presence/avatar.tsx +++ b/packages/editor/src/components/collaborators-presence/avatar.tsx @@ -1,3 +1,4 @@ +import { clsx } from 'clsx'; import { type PostEditorAwarenessState } from '../../../../core-data/src/awareness/types'; import './styles/avatar.scss'; @@ -22,25 +23,22 @@ export const Avatar: React.FC< AvatarProps > = ( { showUserColorBorder, size = 'small', } ) => { - const className = [ + const className = clsx( 'editor-collaborators-presence__avatar', `editor-collaborators-presence__avatar--${ size }`, showUserColorBorder && - 'editor-collaborators-presence__avatar--with-color-border', - ] - .filter( Boolean ) - .join( ' ' ); + 'editor-collaborators-presence__avatar--with-color-border' + ); const avatarUrl = userInfo.avatar_urls?.[ 48 ] || userInfo.avatar_urls?.[ 96 ] || userInfo.avatar_urls?.[ 24 ]; - const avatarStyles: React.CSSProperties & - Record< `--${ string }`, string > = { + const avatarStyles = { '--avatar-url': `url(${ avatarUrl })`, '--user-color': userInfo.color, - }; + } as React.CSSProperties; return (
Date: Fri, 30 Jan 2026 12:31:29 -0600 Subject: [PATCH 11/14] Updated props --- .../components/collaborators-presence/index.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/editor/src/components/collaborators-presence/index.tsx b/packages/editor/src/components/collaborators-presence/index.tsx index b216001d109303..88e2253259a3b0 100644 --- a/packages/editor/src/components/collaborators-presence/index.tsx +++ b/packages/editor/src/components/collaborators-presence/index.tsx @@ -11,7 +11,7 @@ import './styles/collaborators-presence.scss'; const { useActiveUsers } = unlock( privateApis ); -interface AvatarsProps { +interface CollaboratorsPresenceProps { postId: number | null; postType: string | null; } @@ -19,11 +19,15 @@ interface AvatarsProps { /** * Renders a list of avatars for the active users, with a maximum of 3 visible avatars. * Shows a popover with all users on hover. - * @param root0 Component props - * @param root0.postId The ID of the post - * @param root0.postType The type of the post + * + * @param {Object} props CollaboratorsPresence component props + * @param {number} props.postId ID of the post + * @param {string} props.postType Type of the post */ -export function CollaboratorsPresence( { postId, postType }: AvatarsProps ) { +export const CollaboratorsPresence: React.FC< CollaboratorsPresenceProps > = ( { + postId, + postType, +} ) => { const activeUsers = useActiveUsers( postId, postType @@ -87,4 +91,4 @@ export function CollaboratorsPresence( { postId, postType }: AvatarsProps ) { ) }
) : null; -} +}; From cc07500d32f354fac4e2de2ca7e82525d2719db4 Mon Sep 17 00:00:00 2001 From: Max Schmeling Date: Fri, 30 Jan 2026 12:39:36 -0600 Subject: [PATCH 12/14] Localize strings in collaborators list --- .../src/components/collaborators-presence/list.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/editor/src/components/collaborators-presence/list.tsx b/packages/editor/src/components/collaborators-presence/list.tsx index c0af3c08fe7b09..5a739678e63168 100644 --- a/packages/editor/src/components/collaborators-presence/list.tsx +++ b/packages/editor/src/components/collaborators-presence/list.tsx @@ -1,3 +1,4 @@ +import { __ } from '@wordpress/i18n'; import { Popover, Button } from '@wordpress/components'; import { close } from '@wordpress/icons'; @@ -15,10 +16,10 @@ interface CollaboratorsListProps { /** * Renders a list showing all active collaborators with their details. * Note: activeUsers should already exclude the current user (filtered by parent component). - * @param root0 Component props - * @param root0.activeUsers List of active users - * @param root0.popoverAnchor Anchor element for the popover - * @param root0.setIsPopoverVisible Callback to set the visibility of the popover + * @param props Component props + * @param props.activeUsers List of active users + * @param props.popoverAnchor Anchor element for the popover + * @param props.setIsPopoverVisible Callback to set the visibility of the popover */ export const CollaboratorsList: React.FC< CollaboratorsListProps > = ( { activeUsers, @@ -35,7 +36,7 @@ export const CollaboratorsList: React.FC< CollaboratorsListProps > = ( {
- Collaborators + { __( 'Collaborators' ) } { activeUsers.length }
@@ -43,7 +44,7 @@ export const CollaboratorsList: React.FC< CollaboratorsListProps > = ( { __next40pxDefaultSize icon={ close } iconSize={ 16 } - label="Close Collaborators List" + label={ __( 'Close Collaborators List' ) } onClick={ () => setIsPopoverVisible( false ) } />
From 8f0a8ddb9a97cde25bf820cf056033efd6104e15 Mon Sep 17 00:00:00 2001 From: Max Schmeling Date: Fri, 30 Jan 2026 13:42:09 -0600 Subject: [PATCH 13/14] React.FC => function --- .../collaborators-presence/avatar.tsx | 6 +- .../collaborators-presence/index.tsx | 6 +- .../collaborators-presence/list.tsx | 96 ++++++++++--------- 3 files changed, 56 insertions(+), 52 deletions(-) diff --git a/packages/editor/src/components/collaborators-presence/avatar.tsx b/packages/editor/src/components/collaborators-presence/avatar.tsx index 57eb19f30f6922..73cfe7ade43d0f 100644 --- a/packages/editor/src/components/collaborators-presence/avatar.tsx +++ b/packages/editor/src/components/collaborators-presence/avatar.tsx @@ -18,11 +18,11 @@ interface AvatarProps { * @param {boolean} props.showUserColorBorder Whether to show the user color border. * @param {string} props.size Size of the avatar. */ -export const Avatar: React.FC< AvatarProps > = ( { +export function Avatar( { userInfo, showUserColorBorder, size = 'small', -} ) => { +}: AvatarProps ) { const className = clsx( 'editor-collaborators-presence__avatar', `editor-collaborators-presence__avatar--${ size }`, @@ -47,4 +47,4 @@ export const Avatar: React.FC< AvatarProps > = ( { aria-hidden="true" /> ); -}; +} diff --git a/packages/editor/src/components/collaborators-presence/index.tsx b/packages/editor/src/components/collaborators-presence/index.tsx index 88e2253259a3b0..3316906dd77b75 100644 --- a/packages/editor/src/components/collaborators-presence/index.tsx +++ b/packages/editor/src/components/collaborators-presence/index.tsx @@ -24,10 +24,10 @@ interface CollaboratorsPresenceProps { * @param {number} props.postId ID of the post * @param {string} props.postType Type of the post */ -export const CollaboratorsPresence: React.FC< CollaboratorsPresenceProps > = ( { +export function CollaboratorsPresence( { postId, postType, -} ) => { +}: CollaboratorsPresenceProps ) { const activeUsers = useActiveUsers( postId, postType @@ -91,4 +91,4 @@ export const CollaboratorsPresence: React.FC< CollaboratorsPresenceProps > = ( { ) }
) : null; -}; +} diff --git a/packages/editor/src/components/collaborators-presence/list.tsx b/packages/editor/src/components/collaborators-presence/list.tsx index 5a739678e63168..4040f94099ca61 100644 --- a/packages/editor/src/components/collaborators-presence/list.tsx +++ b/packages/editor/src/components/collaborators-presence/list.tsx @@ -21,55 +21,59 @@ interface CollaboratorsListProps { * @param props.popoverAnchor Anchor element for the popover * @param props.setIsPopoverVisible Callback to set the visibility of the popover */ -export const CollaboratorsList: React.FC< CollaboratorsListProps > = ( { +export function CollaboratorsList( { activeUsers, popoverAnchor, setIsPopoverVisible, -} ) => ( - setIsPopoverVisible( false ) } - > -
-
-
- { __( 'Collaborators' ) } - { activeUsers.length } -
-
-
-
-
- { activeUsers.map( ( userState ) => ( -
- - ) ) } + + ) ) } +
-
-
-); + + ); +} From 5b48e526dae12930cd6f6013c753517d34b60555 Mon Sep 17 00:00:00 2001 From: Max Schmeling Date: Tue, 3 Feb 2026 10:16:36 +0100 Subject: [PATCH 14/14] useActiveUsers => useActiveCollaborators --- .../core-data/src/hooks/use-post-editor-awareness-state.ts | 2 +- packages/core-data/src/private-apis.js | 4 ++-- .../editor/src/components/collaborators-presence/index.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core-data/src/hooks/use-post-editor-awareness-state.ts b/packages/core-data/src/hooks/use-post-editor-awareness-state.ts index 5acd74939313a9..3b64ade4cd0211 100644 --- a/packages/core-data/src/hooks/use-post-editor-awareness-state.ts +++ b/packages/core-data/src/hooks/use-post-editor-awareness-state.ts @@ -96,7 +96,7 @@ function usePostEditorAwarenessState( * @param postType - The type of the post. * @return {ActiveUser[]} The active users. */ -export function useActiveUsers( +export function useActiveCollaborators( postId: number | null, postType: string | null ): ActiveUser[] { diff --git a/packages/core-data/src/private-apis.js b/packages/core-data/src/private-apis.js index 8c963572435bfc..917af885a9b8cd 100644 --- a/packages/core-data/src/private-apis.js +++ b/packages/core-data/src/private-apis.js @@ -3,12 +3,12 @@ */ import { useEntityRecordsWithPermissions } from './hooks/use-entity-records'; import { RECEIVE_INTERMEDIATE_RESULTS } from './utils'; -import { useActiveUsers } from './hooks/use-post-editor-awareness-state'; +import { useActiveCollaborators } from './hooks/use-post-editor-awareness-state'; import { lock } from './lock-unlock'; export const privateApis = {}; lock( privateApis, { useEntityRecordsWithPermissions, RECEIVE_INTERMEDIATE_RESULTS, - useActiveUsers, + useActiveCollaborators, } ); diff --git a/packages/editor/src/components/collaborators-presence/index.tsx b/packages/editor/src/components/collaborators-presence/index.tsx index 3316906dd77b75..334ccb415ea682 100644 --- a/packages/editor/src/components/collaborators-presence/index.tsx +++ b/packages/editor/src/components/collaborators-presence/index.tsx @@ -9,7 +9,7 @@ import { type PostEditorAwarenessState } from '../../../../core-data/src/awarene import './styles/collaborators-presence.scss'; -const { useActiveUsers } = unlock( privateApis ); +const { useActiveCollaborators } = unlock( privateApis ); interface CollaboratorsPresenceProps { postId: number | null; @@ -28,7 +28,7 @@ export function CollaboratorsPresence( { postId, postType, }: CollaboratorsPresenceProps ) { - const activeUsers = useActiveUsers( + const activeUsers = useActiveCollaborators( postId, postType ) as PostEditorAwarenessState[];