diff --git a/packages/collaboration-extension/package.json b/packages/collaboration-extension/package.json index 5051d90c..3a06b1c7 100644 --- a/packages/collaboration-extension/package.json +++ b/packages/collaboration-extension/package.json @@ -56,7 +56,7 @@ "@jupyter/collaboration": "^3.1.0-alpha.0", "@jupyter/collaborative-drive": "^3.1.0-alpha.0", "@jupyter/docprovider": "^3.1.0-alpha.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/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/collaboration/src/users-item.tsx b/packages/collaboration/src/users-item.tsx new file mode 100644 index 00000000..122f7a8f --- /dev/null +++ b/packages/collaboration/src/users-item.tsx @@ -0,0 +1,201 @@ +/* + * 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 { classes, ReactWidget } from '@jupyterlab/ui-components'; +import * as React from 'react'; + +const USERS_ITEM_CLASS = 'jp-toolbar-users-item'; + +/** + * The namespace for the UsersItem component. + */ +export namespace UsersItem { + /** + * Properties of the component. + */ + export interface IProps { + /** + * The model of the document. + */ + model: DocumentRegistry.IModel | null; + + /** + * 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 ?? DefaultIconRenderer; + return ( +
+ {this.filterDuplicated(this.state.usersList).map(user => { + if ( + this._model && + user.userId !== this._model.sharedModel.awareness.clientID + ) { + return IconRenderer({ user, model: this._model }); + } + })} +
+ ); + } + + /** + * 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 | null; + private _iconRenderer: + | ((props: UsersItem.IIconRendererProps) => JSX.Element) + | null; +} + +/** + * Default renderer for the user icon. + */ +export function DefaultIconRenderer( + props: UsersItem.IIconRendererProps +): JSX.Element { + let el: JSX.Element; + + // 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 = ( +
+ {user.userData.initials} +
+ ); + } + + return el; +} 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..1bff7903 --- /dev/null +++ b/packages/collaboration/style/users-item.css @@ -0,0 +1,20 @@ +/* ----------------------------------------------------------------------------- +| 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; +} + +.jp-toolbar-users-item .jp-MenuBar-anonymousIcon, +.jp-toolbar-users-item .jp-MenuBar-imageIcon { + position: relative; + left: 0; + height: 22px; + width: 22px; + box-sizing: border-box; + cursor: default; +} diff --git a/packages/collaborative-drive/package.json b/packages/collaborative-drive/package.json index a74d7adf..bb7277a8 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 ceed5257..65f07991 100644 --- a/packages/docprovider-extension/package.json +++ b/packages/docprovider-extension/package.json @@ -55,7 +55,7 @@ "dependencies": { "@jupyter/collaborative-drive": "^3.1.0-alpha.0", "@jupyter/docprovider": "^3.1.0-alpha.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 10b04339..3f165f83 100644 --- a/packages/docprovider/package.json +++ b/packages/docprovider/package.json @@ -42,7 +42,7 @@ }, "dependencies": { "@jupyter/collaborative-drive": "^3.1.0-alpha.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/yarn.lock b/yarn.lock index 497f5b83..d3ef212d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2023,7 +2023,7 @@ __metadata: "@jupyter/collaboration": ^3.1.0-alpha.0 "@jupyter/collaborative-drive": ^3.1.0-alpha.0 "@jupyter/docprovider": ^3.1.0-alpha.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-alpha.0 "@jupyter/docprovider": ^3.1.0-alpha.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-alpha.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,7 +2181,21 @@ __metadata: languageName: node linkType: hard -"@jupyter/ydoc@npm:^2.0.0 || ^3.0.0, @jupyter/ydoc@npm:^3.0.0": +"@jupyter/ydoc@npm:^2.1.3 || ^3.0.0": + version: 3.0.1 + resolution: "@jupyter/ydoc@npm:3.0.1" + dependencies: + "@jupyterlab/nbformat": ^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0 + "@lumino/coreutils": ^1.11.0 || ^2.0.0 + "@lumino/disposable": ^1.10.0 || ^2.0.0 + "@lumino/signaling": ^1.10.0 || ^2.0.0 + y-protocols: ^1.0.5 + yjs: ^13.5.40 + checksum: 4e14c40d81ee3c57cc0d84eb90d06f1bd98a18718f07510e918d12ae645a700b4f04c8862c1addf113eeab85102643324870598eea718f52d627f58b22e81977 + languageName: node + linkType: hard + +"@jupyter/ydoc@npm:^3.0.0": version: 3.0.0 resolution: "@jupyter/ydoc@npm:3.0.0" dependencies: