Skip to content

Commit

Permalink
Users item toolbar (#379)
Browse files Browse the repository at this point in the history
* Provide a widget that can be used in a collaborative document to display current users

* Automatic application of license header

* Move the widget to collaboration package

* Add style and allow empty model (for notebook panel for example)

* Send the document model to the iconRenderer, and avoid displaying cursor on Anonymous icons

* 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

* CSS

* lint

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
brichet and github-actions[bot] authored Dec 10, 2024
1 parent 47305e4 commit 22a505b
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 12 deletions.
2 changes: 1 addition & 1 deletion packages/collaboration-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/collaboration/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './cursors';
export * from './menu';
export * from './sharedlink';
export * from './userinfopanel';
export * from './users-item';
201 changes: 201 additions & 0 deletions packages/collaboration/src/users-item.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLElement> {
/**
* 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(<UsersItem {...options} />);
}

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<string>();
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 (
<div className={USERS_ITEM_CLASS}>
{this.filterDuplicated(this.state.usersList).map(user => {
if (
this._model &&
user.userId !== this._model.sharedModel.awareness.clientID
) {
return IconRenderer({ user, model: this._model });
}
})}
</div>
);
}

/**
* 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 = (
<div
{...htmlProps}
key={user.userId}
title={user.userData.display_name}
className={classes(iconClasses, 'jp-MenuBar-imageIcon')}
>
<img src={user.userData.avatar_url} alt="" />
</div>
);
} else {
el = (
<div
{...htmlProps}
key={user.userId}
title={user.userData.display_name}
className={classes(iconClasses, 'jp-MenuBar-anonymousIcon')}
style={{ backgroundColor: user.userData.color }}
>
<span>{user.userData.initials}</span>
</div>
);
}

return el;
}
1 change: 1 addition & 0 deletions packages/collaboration/style/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

@import url('./menu.css');
@import url('./sidepanel.css');
@import url('./users-item.css');

.jp-shared-link-body {
user-select: none;
Expand Down
20 changes: 20 additions & 0 deletions packages/collaboration/style/users-item.css
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 1 addition & 1 deletion packages/collaborative-drive/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion packages/docprovider-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/docprovider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 8 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -2181,17 +2181,17 @@ __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
"@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: e9419a461f33d2685db346b19806865fe37f61b2ca33eb39c4ea905d765794a928442adf1bbffda67b665bdeba3be9a082189a57eaab5367aeaf6b57caeda822
checksum: 770f73459635c74bd0e5cacdca1ea1f77ee8efd6e7cd58f0ccbb167ae8374e73118620f4f3628646281160a7bc7389f374bd2106f1e799bdc8f78cad0ce05b28
languageName: node
linkType: hard

Expand Down

0 comments on commit 22a505b

Please sign in to comment.