From 6e4f350acddec813a5e20d22b719e2bb715f0b79 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Fri, 18 Oct 2024 16:24:04 +0200 Subject: [PATCH 1/8] Provide a widget that can be used in a collaborative document to display current users --- packages/collaboration-extension/package.json | 2 +- packages/collaborative-drive/package.json | 2 +- packages/docprovider-extension/package.json | 2 +- packages/docprovider/package.json | 2 +- packages/docprovider/src/index.ts | 1 + packages/docprovider/src/users-item.tsx | 187 ++++++++++++++++++ yarn.lock | 16 +- 7 files changed, 200 insertions(+), 12 deletions(-) create mode 100644 packages/docprovider/src/users-item.tsx diff --git a/packages/collaboration-extension/package.json b/packages/collaboration-extension/package.json index 4ce41778..bfefe17b 100644 --- a/packages/collaboration-extension/package.json +++ b/packages/collaboration-extension/package.json @@ -56,7 +56,7 @@ "@jupyter/collaboration": "^3.1.0", "@jupyter/collaborative-drive": "^3.1.0", "@jupyter/docprovider": "^3.1.0", - "@jupyter/ydoc": "^2.0.0 || ^3.0.0", + "@jupyter/ydoc": "^2.1.3 || ^3.0.0", "@jupyterlab/application": "^4.2.0", "@jupyterlab/apputils": "^4.2.0", "@jupyterlab/codemirror": "^4.2.0", diff --git a/packages/collaborative-drive/package.json b/packages/collaborative-drive/package.json index 19a7e8c1..1f29270a 100644 --- a/packages/collaborative-drive/package.json +++ b/packages/collaborative-drive/package.json @@ -37,7 +37,7 @@ "watch": "tsc -b --watch" }, "dependencies": { - "@jupyter/ydoc": "^2.0.0 || ^3.0.0", + "@jupyter/ydoc": "^2.1.3 || ^3.0.0", "@jupyterlab/services": "^7.2.0", "@lumino/coreutils": "^2.1.0", "@lumino/disposable": "^2.1.0" diff --git a/packages/docprovider-extension/package.json b/packages/docprovider-extension/package.json index 27bc699f..ed0c3175 100644 --- a/packages/docprovider-extension/package.json +++ b/packages/docprovider-extension/package.json @@ -55,7 +55,7 @@ "dependencies": { "@jupyter/collaborative-drive": "^3.1.0", "@jupyter/docprovider": "^3.1.0", - "@jupyter/ydoc": "^2.0.0 || ^3.0.0", + "@jupyter/ydoc": "^2.1.3 || ^3.0.0", "@jupyterlab/application": "^4.2.0", "@jupyterlab/apputils": "^4.2.0", "@jupyterlab/docregistry": "^4.2.0", diff --git a/packages/docprovider/package.json b/packages/docprovider/package.json index d9d91e25..0a3fe999 100644 --- a/packages/docprovider/package.json +++ b/packages/docprovider/package.json @@ -42,7 +42,7 @@ }, "dependencies": { "@jupyter/collaborative-drive": "^3.1.0", - "@jupyter/ydoc": "^2.0.0 || ^3.0.0", + "@jupyter/ydoc": "^2.1.3 || ^3.0.0", "@jupyterlab/apputils": "^4.2.0", "@jupyterlab/cells": "^4.2.0", "@jupyterlab/coreutils": "^6.2.0", diff --git a/packages/docprovider/src/index.ts b/packages/docprovider/src/index.ts index 178b7984..f91bb64e 100644 --- a/packages/docprovider/src/index.ts +++ b/packages/docprovider/src/index.ts @@ -15,3 +15,4 @@ export * from './yprovider'; export * from './TimelineSlider'; export * from './tokens'; export * from './forkManager'; +export * from './users-item'; diff --git a/packages/docprovider/src/users-item.tsx b/packages/docprovider/src/users-item.tsx new file mode 100644 index 00000000..06733c7b --- /dev/null +++ b/packages/docprovider/src/users-item.tsx @@ -0,0 +1,187 @@ +import { DocumentRegistry } from '@jupyterlab/docregistry'; +import { User } from '@jupyterlab/services'; +import { ReactWidget } from '@jupyterlab/ui-components'; +import * as React from 'react'; + +/** + * The namespace for the UsersItem component. + */ +export namespace UsersItem { + /** + * Properties of the component. + */ + export interface IProps { + /** + * The model of the document. + */ + model: DocumentRegistry.IModel; + + /** + * A function to display the user icons, optional. + * This function will overwrite the default one, and can be used to handle event on + * icons. + */ + iconRenderer?: (props: UsersItem.IIconRendererProps) => JSX.Element; + } + + /** + * The state of the component. + */ + export interface IState { + /** + * The user list. + */ + usersList: IUserData[]; + } + + /** + * Properties send to the iconRenderer function. + */ + export interface IIconRendererProps + extends React.HTMLAttributes { + /** + * The user. + */ + user: IUserData; + + /** + * The document's model. + */ + model?: DocumentRegistry.IModel; + } + + /** + * The user data type. + */ + export type IUserData = { + /** + * User id (the client id of the awareness). + */ + userId: number; + /** + * User data. + */ + userData: User.IIdentity; + }; +} + +/** + * A component displaying the collaborative users of a document. + */ +export class UsersItem extends React.Component< + UsersItem.IProps, + UsersItem.IState +> { + constructor(props: UsersItem.IProps) { + super(props); + this._model = props.model; + this._iconRenderer = props.iconRenderer ?? null; + this.state = { usersList: [] }; + } + + /** + * Static method to create a widget. + */ + static createWidget(options: UsersItem.IProps): ReactWidget { + return ReactWidget.create(); + } + + componentDidMount(): void { + this._model.sharedModel.awareness.on('change', this._awarenessChange); + this._awarenessChange(); + } + + /** + * Filter out the duplicated users, which can happen temporary on reload. + */ + private filterDuplicated( + usersList: UsersItem.IUserData[] + ): UsersItem.IUserData[] { + const newList: UsersItem.IUserData[] = []; + const selected = new Set(); + for (const element of usersList) { + if ( + element?.userData?.username && + !selected.has(element.userData.username) + ) { + selected.add(element.userData.username); + newList.push(element); + } + } + return newList; + } + + render(): React.ReactNode { + const IconRenderer = this._iconRenderer ?? DefaultUserIcon; + return ( +
+ {this.filterDuplicated(this.state.usersList).map(user => { + if (user.userId !== this._model.sharedModel.awareness.clientID) { + return IconRenderer({ user }); + } + })} +
+ ); + } + + /** + * Triggered when a change occurs in the document awareness, to build again the users list. + */ + private _awarenessChange = () => { + const clients = this._model.sharedModel.awareness.getStates() as Map< + number, + User.IIdentity + >; + + const users: UsersItem.IUserData[] = []; + if (clients) { + clients.forEach((val, key) => { + if (val.user) { + users.push({ userId: key, userData: val.user as User.IIdentity }); + } + }); + } + this.setState(old => ({ ...old, usersList: users })); + }; + + private _model: DocumentRegistry.IModel; + private _iconRenderer: + | ((props: UsersItem.IIconRendererProps) => JSX.Element) + | null; +} + +/** + * Default function displaying a user icon. + */ +export function DefaultUserIcon( + props: UsersItem.IIconRendererProps +): JSX.Element { + let el: JSX.Element; + const { userId, userData } = props.user; + if (userData.avatar_url) { + el = ( +
+ +
+ ); + } else { + el = ( +
+ {userData.initials} +
+ ); + } + + return el; +} diff --git a/yarn.lock b/yarn.lock index 92ecbd2b..8ff49546 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2023,7 +2023,7 @@ __metadata: "@jupyter/collaboration": ^3.1.0 "@jupyter/collaborative-drive": ^3.1.0 "@jupyter/docprovider": ^3.1.0 - "@jupyter/ydoc": ^2.0.0 || ^3.0.0 + "@jupyter/ydoc": ^2.1.3 || ^3.0.0 "@jupyterlab/application": ^4.2.0 "@jupyterlab/apputils": ^4.2.0 "@jupyterlab/builder": ^4.0.5 @@ -2072,7 +2072,7 @@ __metadata: version: 0.0.0-use.local resolution: "@jupyter/collaborative-drive@workspace:packages/collaborative-drive" dependencies: - "@jupyter/ydoc": ^2.0.0 || ^3.0.0 + "@jupyter/ydoc": ^2.1.3 || ^3.0.0 "@jupyterlab/services": ^7.2.0 "@lumino/coreutils": ^2.1.0 "@lumino/disposable": ^2.1.0 @@ -2087,7 +2087,7 @@ __metadata: dependencies: "@jupyter/collaborative-drive": ^3.1.0 "@jupyter/docprovider": ^3.1.0 - "@jupyter/ydoc": ^2.0.0 || ^3.0.0 + "@jupyter/ydoc": ^2.1.3 || ^3.0.0 "@jupyterlab/application": ^4.2.0 "@jupyterlab/apputils": ^4.2.0 "@jupyterlab/builder": ^4.0.0 @@ -2114,7 +2114,7 @@ __metadata: resolution: "@jupyter/docprovider@workspace:packages/docprovider" dependencies: "@jupyter/collaborative-drive": ^3.1.0 - "@jupyter/ydoc": ^2.0.0 || ^3.0.0 + "@jupyter/ydoc": ^2.1.3 || ^3.0.0 "@jupyterlab/apputils": ^4.2.0 "@jupyterlab/cells": ^4.2.0 "@jupyterlab/coreutils": ^6.2.0 @@ -2181,9 +2181,9 @@ __metadata: languageName: node linkType: hard -"@jupyter/ydoc@npm:^2.0.0 || ^3.0.0, @jupyter/ydoc@npm:^3.0.0": - version: 3.0.0 - resolution: "@jupyter/ydoc@npm:3.0.0" +"@jupyter/ydoc@npm:^2.1.3 || ^3.0.0, @jupyter/ydoc@npm:^3.0.0": + version: 3.0.2 + resolution: "@jupyter/ydoc@npm:3.0.2" dependencies: "@jupyterlab/nbformat": ^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0 "@lumino/coreutils": ^1.11.0 || ^2.0.0 @@ -2191,7 +2191,7 @@ __metadata: "@lumino/signaling": ^1.10.0 || ^2.0.0 y-protocols: ^1.0.5 yjs: ^13.5.40 - checksum: e9419a461f33d2685db346b19806865fe37f61b2ca33eb39c4ea905d765794a928442adf1bbffda67b665bdeba3be9a082189a57eaab5367aeaf6b57caeda822 + checksum: 770f73459635c74bd0e5cacdca1ea1f77ee8efd6e7cd58f0ccbb167ae8374e73118620f4f3628646281160a7bc7389f374bd2106f1e799bdc8f78cad0ce05b28 languageName: node linkType: hard From 737388db1893df912e33e085f59b8d1bc7aa1e39 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 18 Oct 2024 14:28:30 +0000 Subject: [PATCH 2/8] Automatic application of license header --- packages/docprovider/src/users-item.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/docprovider/src/users-item.tsx b/packages/docprovider/src/users-item.tsx index 06733c7b..6e44de9b 100644 --- a/packages/docprovider/src/users-item.tsx +++ b/packages/docprovider/src/users-item.tsx @@ -1,3 +1,8 @@ +/* + * Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ + import { DocumentRegistry } from '@jupyterlab/docregistry'; import { User } from '@jupyterlab/services'; import { ReactWidget } from '@jupyterlab/ui-components'; From e8c0cc78593c04f006d7ba6ec79e1f57f92b50bc Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Mon, 21 Oct 2024 15:43:58 +0200 Subject: [PATCH 3/8] Move the widget to collaboration package --- packages/collaboration/src/index.ts | 1 + packages/{docprovider => collaboration}/src/users-item.tsx | 0 packages/docprovider/src/index.ts | 1 - 3 files changed, 1 insertion(+), 1 deletion(-) rename packages/{docprovider => collaboration}/src/users-item.tsx (100%) diff --git a/packages/collaboration/src/index.ts b/packages/collaboration/src/index.ts index 05e4fff7..8b8209ca 100644 --- a/packages/collaboration/src/index.ts +++ b/packages/collaboration/src/index.ts @@ -11,3 +11,4 @@ export * from './cursors'; export * from './menu'; export * from './sharedlink'; export * from './userinfopanel'; +export * from './users-item'; diff --git a/packages/docprovider/src/users-item.tsx b/packages/collaboration/src/users-item.tsx similarity index 100% rename from packages/docprovider/src/users-item.tsx rename to packages/collaboration/src/users-item.tsx diff --git a/packages/docprovider/src/index.ts b/packages/docprovider/src/index.ts index f91bb64e..178b7984 100644 --- a/packages/docprovider/src/index.ts +++ b/packages/docprovider/src/index.ts @@ -15,4 +15,3 @@ export * from './yprovider'; export * from './TimelineSlider'; export * from './tokens'; export * from './forkManager'; -export * from './users-item'; From 801c686ee3c33420a76bed7ff6578ce23217b46b Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Tue, 22 Oct 2024 14:54:59 +0200 Subject: [PATCH 4/8] Add style and allow empty model (for notebook panel for example) --- packages/collaboration/src/users-item.tsx | 17 +++++++++++------ packages/collaboration/style/base.css | 1 + packages/collaboration/style/users-item.css | 19 +++++++++++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 packages/collaboration/style/users-item.css diff --git a/packages/collaboration/src/users-item.tsx b/packages/collaboration/src/users-item.tsx index 6e44de9b..85238d03 100644 --- a/packages/collaboration/src/users-item.tsx +++ b/packages/collaboration/src/users-item.tsx @@ -8,6 +8,8 @@ import { User } from '@jupyterlab/services'; import { ReactWidget } from '@jupyterlab/ui-components'; import * as React from 'react'; +const USERS_ITEM_CLASS = 'jp-toolbar-users-item'; + /** * The namespace for the UsersItem component. */ @@ -19,7 +21,7 @@ export namespace UsersItem { /** * The model of the document. */ - model: DocumentRegistry.IModel; + model: DocumentRegistry.IModel | null; /** * A function to display the user icons, optional. @@ -92,7 +94,7 @@ export class UsersItem extends React.Component< } componentDidMount(): void { - this._model.sharedModel.awareness.on('change', this._awarenessChange); + this._model?.sharedModel.awareness.on('change', this._awarenessChange); this._awarenessChange(); } @@ -119,9 +121,12 @@ export class UsersItem extends React.Component< render(): React.ReactNode { const IconRenderer = this._iconRenderer ?? DefaultUserIcon; return ( -
+
{this.filterDuplicated(this.state.usersList).map(user => { - if (user.userId !== this._model.sharedModel.awareness.clientID) { + if ( + this._model && + user.userId !== this._model.sharedModel.awareness.clientID + ) { return IconRenderer({ user }); } })} @@ -133,7 +138,7 @@ export class UsersItem extends React.Component< * Triggered when a change occurs in the document awareness, to build again the users list. */ private _awarenessChange = () => { - const clients = this._model.sharedModel.awareness.getStates() as Map< + const clients = this._model?.sharedModel.awareness.getStates() as Map< number, User.IIdentity >; @@ -149,7 +154,7 @@ export class UsersItem extends React.Component< this.setState(old => ({ ...old, usersList: users })); }; - private _model: DocumentRegistry.IModel; + private _model: DocumentRegistry.IModel | null; private _iconRenderer: | ((props: UsersItem.IIconRendererProps) => JSX.Element) | null; diff --git a/packages/collaboration/style/base.css b/packages/collaboration/style/base.css index 41d787b5..792ab3ac 100644 --- a/packages/collaboration/style/base.css +++ b/packages/collaboration/style/base.css @@ -5,6 +5,7 @@ @import url('./menu.css'); @import url('./sidepanel.css'); +@import url('./users-item.css'); .jp-shared-link-body { user-select: none; diff --git a/packages/collaboration/style/users-item.css b/packages/collaboration/style/users-item.css new file mode 100644 index 00000000..db4930fb --- /dev/null +++ b/packages/collaboration/style/users-item.css @@ -0,0 +1,19 @@ +/* ----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|---------------------------------------------------------------------------- */ + +.jp-toolbar-users-item { + flex-grow: 1; + display: flex; + flex-direction: row-reverse; +} + +.jp-toolbar-users-item .jp-MenuBar-anonymousIcon, +.jp-toolbar-users-item .jp-MenuBar-imageIcon { + position: unset !important; + margin-right: 2px; + height: 22px; + width: 22px; + box-sizing: border-box; +} From 8a0ebdd2e3bc1dbfab3f2746ea6077a7627af756 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Fri, 25 Oct 2024 13:53:54 +0200 Subject: [PATCH 5/8] Send the document model to the iconRenderer, and avoid displaying cursor on Anonymous icons --- packages/collaboration/src/users-item.tsx | 2 +- packages/collaboration/style/users-item.css | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/collaboration/src/users-item.tsx b/packages/collaboration/src/users-item.tsx index 85238d03..23debf6b 100644 --- a/packages/collaboration/src/users-item.tsx +++ b/packages/collaboration/src/users-item.tsx @@ -127,7 +127,7 @@ export class UsersItem extends React.Component< this._model && user.userId !== this._model.sharedModel.awareness.clientID ) { - return IconRenderer({ user }); + return IconRenderer({ user, model: this._model }); } })}
diff --git a/packages/collaboration/style/users-item.css b/packages/collaboration/style/users-item.css index db4930fb..6358e35e 100644 --- a/packages/collaboration/style/users-item.css +++ b/packages/collaboration/style/users-item.css @@ -16,4 +16,5 @@ height: 22px; width: 22px; box-sizing: border-box; + cursor: default; } From 54401b34cde6637d69cdc6680e8b7070834e983e Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Fri, 25 Oct 2024 17:09:23 +0200 Subject: [PATCH 6/8] Improve the default icon renderer - allow the default renderer to receive additional classes - rename it for consistency - avoid sending non related props to div element --- packages/collaboration/src/users-item.tsx | 38 +++++++++++++---------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/packages/collaboration/src/users-item.tsx b/packages/collaboration/src/users-item.tsx index 23debf6b..122f7a8f 100644 --- a/packages/collaboration/src/users-item.tsx +++ b/packages/collaboration/src/users-item.tsx @@ -5,7 +5,7 @@ import { DocumentRegistry } from '@jupyterlab/docregistry'; import { User } from '@jupyterlab/services'; -import { ReactWidget } from '@jupyterlab/ui-components'; +import { classes, ReactWidget } from '@jupyterlab/ui-components'; import * as React from 'react'; const USERS_ITEM_CLASS = 'jp-toolbar-users-item'; @@ -119,7 +119,7 @@ export class UsersItem extends React.Component< } render(): React.ReactNode { - const IconRenderer = this._iconRenderer ?? DefaultUserIcon; + const IconRenderer = this._iconRenderer ?? DefaultIconRenderer; return (
{this.filterDuplicated(this.state.usersList).map(user => { @@ -161,34 +161,38 @@ export class UsersItem extends React.Component< } /** - * Default function displaying a user icon. + * Default renderer for the user icon. */ -export function DefaultUserIcon( +export function DefaultIconRenderer( props: UsersItem.IIconRendererProps ): JSX.Element { let el: JSX.Element; - const { userId, userData } = props.user; - if (userData.avatar_url) { + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { user, model, ...htmlProps } = props; + + const iconClasses = classes('lm-MenuBar-itemIcon', props.className || ''); + if (user.userData.avatar_url) { el = (
- +
); } else { el = (
- {userData.initials} + {user.userData.initials}
); } From 1ef3a5159011fc83cb0b3f3174c8e6e1dee5b479 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Fri, 25 Oct 2024 19:21:02 +0200 Subject: [PATCH 7/8] CSS --- packages/collaboration/style/users-item.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/collaboration/style/users-item.css b/packages/collaboration/style/users-item.css index 6358e35e..eb845d6b 100644 --- a/packages/collaboration/style/users-item.css +++ b/packages/collaboration/style/users-item.css @@ -6,13 +6,13 @@ .jp-toolbar-users-item { flex-grow: 1; display: flex; - flex-direction: row-reverse; + flex-direction: row; } .jp-toolbar-users-item .jp-MenuBar-anonymousIcon, .jp-toolbar-users-item .jp-MenuBar-imageIcon { - position: unset !important; - margin-right: 2px; + position: relative; + left: 0px; height: 22px; width: 22px; box-sizing: border-box; From e588df748a8b931975481e8e81a52735414c23a4 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Mon, 28 Oct 2024 09:23:17 +0100 Subject: [PATCH 8/8] lint --- packages/collaboration/style/users-item.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/collaboration/style/users-item.css b/packages/collaboration/style/users-item.css index eb845d6b..1bff7903 100644 --- a/packages/collaboration/style/users-item.css +++ b/packages/collaboration/style/users-item.css @@ -12,7 +12,7 @@ .jp-toolbar-users-item .jp-MenuBar-anonymousIcon, .jp-toolbar-users-item .jp-MenuBar-imageIcon { position: relative; - left: 0px; + left: 0; height: 22px; width: 22px; box-sizing: border-box;