diff --git a/packages/collaboration-extension/package.json b/packages/collaboration-extension/package.json index ac0a7a33..82103618 100644 --- a/packages/collaboration-extension/package.json +++ b/packages/collaboration-extension/package.json @@ -56,7 +56,7 @@ "@jupyter/collaboration": "^3.0.0-rc.0", "@jupyter/collaborative-drive": "^3.0.0-rc.0", "@jupyter/docprovider": "^3.0.0-rc.0", - "@jupyter/ydoc": "^2.0.0 || ^3.0.0-a3", + "@jupyter/ydoc": "^2.1.3 || ^3.0.0-b0", "@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 e4220ffe..081c6099 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-a3", + "@jupyter/ydoc": "^2.1.3 || ^3.0.0-b0", "@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 dc7fe25a..1f0ad4b6 100644 --- a/packages/docprovider-extension/package.json +++ b/packages/docprovider-extension/package.json @@ -55,7 +55,7 @@ "dependencies": { "@jupyter/collaborative-drive": "^3.0.0-rc.0", "@jupyter/docprovider": "^3.0.0-rc.0", - "@jupyter/ydoc": "^2.0.0 || ^3.0.0-a3", + "@jupyter/ydoc": "^2.1.3 || ^3.0.0-b0", "@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 a591cde3..2d2d2386 100644 --- a/packages/docprovider/package.json +++ b/packages/docprovider/package.json @@ -42,7 +42,7 @@ }, "dependencies": { "@jupyter/collaborative-drive": "^3.0.0-rc.0", - "@jupyter/ydoc": "^2.0.0 || ^3.0.0-a3", + "@jupyter/ydoc": "^2.1.3 || ^3.0.0-b0", "@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 b3cd6572..d08b29eb 100644 --- a/packages/docprovider/src/index.ts +++ b/packages/docprovider/src/index.ts @@ -13,3 +13,4 @@ export * from './requests'; export * from './ydrive'; export * from './yprovider'; export * from './TimelineSlider'; +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..6e44de9b --- /dev/null +++ b/packages/docprovider/src/users-item.tsx @@ -0,0 +1,192 @@ +/* + * 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'; +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 4724d2ad..4046bf98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2078,7 +2078,7 @@ __metadata: "@jupyter/collaboration": ^3.0.0-rc.0 "@jupyter/collaborative-drive": ^3.0.0-rc.0 "@jupyter/docprovider": ^3.0.0-rc.0 - "@jupyter/ydoc": ^2.0.0 || ^3.0.0-a3 + "@jupyter/ydoc": ^2.1.3 || ^3.0.0-b0 "@jupyterlab/application": ^4.2.0 "@jupyterlab/apputils": ^4.2.0 "@jupyterlab/builder": ^4.0.5 @@ -2127,7 +2127,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-a3 + "@jupyter/ydoc": ^2.1.3 || ^3.0.0-b0 "@jupyterlab/services": ^7.2.0 "@lumino/coreutils": ^2.1.0 "@lumino/disposable": ^2.1.0 @@ -2142,7 +2142,7 @@ __metadata: dependencies: "@jupyter/collaborative-drive": ^3.0.0-rc.0 "@jupyter/docprovider": ^3.0.0-rc.0 - "@jupyter/ydoc": ^2.0.0 || ^3.0.0-a3 + "@jupyter/ydoc": ^2.1.3 || ^3.0.0-b0 "@jupyterlab/application": ^4.2.0 "@jupyterlab/apputils": ^4.2.0 "@jupyterlab/builder": ^4.0.0 @@ -2169,7 +2169,7 @@ __metadata: resolution: "@jupyter/docprovider@workspace:packages/docprovider" dependencies: "@jupyter/collaborative-drive": ^3.0.0-rc.0 - "@jupyter/ydoc": ^2.0.0 || ^3.0.0-a3 + "@jupyter/ydoc": ^2.1.3 || ^3.0.0-b0 "@jupyterlab/apputils": ^4.2.0 "@jupyterlab/cells": ^4.2.0 "@jupyterlab/coreutils": ^6.2.0 @@ -2237,9 +2237,9 @@ __metadata: languageName: node linkType: hard -"@jupyter/ydoc@npm:^2.0.0 || ^3.0.0-a3": - version: 3.0.0-a4 - resolution: "@jupyter/ydoc@npm:3.0.0-a4" +"@jupyter/ydoc@npm:^2.0.1": + version: 2.1.3 + resolution: "@jupyter/ydoc@npm:2.1.3" dependencies: "@jupyterlab/nbformat": ^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0 "@lumino/coreutils": ^1.11.0 || ^2.0.0 @@ -2247,13 +2247,13 @@ __metadata: "@lumino/signaling": ^1.10.0 || ^2.0.0 y-protocols: ^1.0.5 yjs: ^13.5.40 - checksum: ccd4d8b3c46346e14e4e20f093c0147349c403f1a61d624bc01a95fb805f41c8c5c4db54c5c43c59dc2ef7aeebb53a451a7fc75875c144578bf180c0d20c1878 + checksum: 61b38e3f89accebc8060eb0aacc11bf812befb9b1cec085d1b0153be851037a3a26b5576d58e5bc65d8c0697ef9f1e535afa73af1b1deef0523d749ff4ac0ac9 languageName: node linkType: hard -"@jupyter/ydoc@npm:^2.0.1": - version: 2.1.1 - resolution: "@jupyter/ydoc@npm:2.1.1" +"@jupyter/ydoc@npm:^2.1.3 || ^3.0.0-b0": + version: 3.0.0-b0 + resolution: "@jupyter/ydoc@npm:3.0.0-b0" dependencies: "@jupyterlab/nbformat": ^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0 "@lumino/coreutils": ^1.11.0 || ^2.0.0 @@ -2261,7 +2261,7 @@ __metadata: "@lumino/signaling": ^1.10.0 || ^2.0.0 y-protocols: ^1.0.5 yjs: ^13.5.40 - checksum: f10268d4d990f454279e3908a172755ed5885fa81bb70c31bdf66923598b283d26491741bece137d1c348619861e9b7f8354296773fe5352b1915e69101a9fb0 + checksum: 2c9be60abb580f5b6053c1834f7345eb85138d1102042c105b85a72e0fc96801417e7b78139c62a0c8fcf74bed53eabe981f3847fb0f89cdb7e3a90933c15ae8 languageName: node linkType: hard