Skip to content

Commit e133bef

Browse files
Backport PR #379: Users item toolbar (#440)
Co-authored-by: Nicolas Brichet <[email protected]>
1 parent f5ddb94 commit e133bef

File tree

9 files changed

+235
-12
lines changed

9 files changed

+235
-12
lines changed

packages/collaboration-extension/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"@jupyter/collaboration": "^3.1.0",
5757
"@jupyter/collaborative-drive": "^3.1.0",
5858
"@jupyter/docprovider": "^3.1.0",
59-
"@jupyter/ydoc": "^2.0.0 || ^3.0.0",
59+
"@jupyter/ydoc": "^2.1.3 || ^3.0.0",
6060
"@jupyterlab/application": "^4.2.0",
6161
"@jupyterlab/apputils": "^4.2.0",
6262
"@jupyterlab/codemirror": "^4.2.0",

packages/collaboration/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export * from './cursors';
1111
export * from './menu';
1212
export * from './sharedlink';
1313
export * from './userinfopanel';
14+
export * from './users-item';
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/*
2+
* Copyright (c) Jupyter Development Team.
3+
* Distributed under the terms of the Modified BSD License.
4+
*/
5+
6+
import { DocumentRegistry } from '@jupyterlab/docregistry';
7+
import { User } from '@jupyterlab/services';
8+
import { classes, ReactWidget } from '@jupyterlab/ui-components';
9+
import * as React from 'react';
10+
11+
const USERS_ITEM_CLASS = 'jp-toolbar-users-item';
12+
13+
/**
14+
* The namespace for the UsersItem component.
15+
*/
16+
export namespace UsersItem {
17+
/**
18+
* Properties of the component.
19+
*/
20+
export interface IProps {
21+
/**
22+
* The model of the document.
23+
*/
24+
model: DocumentRegistry.IModel | null;
25+
26+
/**
27+
* A function to display the user icons, optional.
28+
* This function will overwrite the default one, and can be used to handle event on
29+
* icons.
30+
*/
31+
iconRenderer?: (props: UsersItem.IIconRendererProps) => JSX.Element;
32+
}
33+
34+
/**
35+
* The state of the component.
36+
*/
37+
export interface IState {
38+
/**
39+
* The user list.
40+
*/
41+
usersList: IUserData[];
42+
}
43+
44+
/**
45+
* Properties send to the iconRenderer function.
46+
*/
47+
export interface IIconRendererProps
48+
extends React.HTMLAttributes<HTMLElement> {
49+
/**
50+
* The user.
51+
*/
52+
user: IUserData;
53+
54+
/**
55+
* The document's model.
56+
*/
57+
model?: DocumentRegistry.IModel;
58+
}
59+
60+
/**
61+
* The user data type.
62+
*/
63+
export type IUserData = {
64+
/**
65+
* User id (the client id of the awareness).
66+
*/
67+
userId: number;
68+
/**
69+
* User data.
70+
*/
71+
userData: User.IIdentity;
72+
};
73+
}
74+
75+
/**
76+
* A component displaying the collaborative users of a document.
77+
*/
78+
export class UsersItem extends React.Component<
79+
UsersItem.IProps,
80+
UsersItem.IState
81+
> {
82+
constructor(props: UsersItem.IProps) {
83+
super(props);
84+
this._model = props.model;
85+
this._iconRenderer = props.iconRenderer ?? null;
86+
this.state = { usersList: [] };
87+
}
88+
89+
/**
90+
* Static method to create a widget.
91+
*/
92+
static createWidget(options: UsersItem.IProps): ReactWidget {
93+
return ReactWidget.create(<UsersItem {...options} />);
94+
}
95+
96+
componentDidMount(): void {
97+
this._model?.sharedModel.awareness.on('change', this._awarenessChange);
98+
this._awarenessChange();
99+
}
100+
101+
/**
102+
* Filter out the duplicated users, which can happen temporary on reload.
103+
*/
104+
private filterDuplicated(
105+
usersList: UsersItem.IUserData[]
106+
): UsersItem.IUserData[] {
107+
const newList: UsersItem.IUserData[] = [];
108+
const selected = new Set<string>();
109+
for (const element of usersList) {
110+
if (
111+
element?.userData?.username &&
112+
!selected.has(element.userData.username)
113+
) {
114+
selected.add(element.userData.username);
115+
newList.push(element);
116+
}
117+
}
118+
return newList;
119+
}
120+
121+
render(): React.ReactNode {
122+
const IconRenderer = this._iconRenderer ?? DefaultIconRenderer;
123+
return (
124+
<div className={USERS_ITEM_CLASS}>
125+
{this.filterDuplicated(this.state.usersList).map(user => {
126+
if (
127+
this._model &&
128+
user.userId !== this._model.sharedModel.awareness.clientID
129+
) {
130+
return IconRenderer({ user, model: this._model });
131+
}
132+
})}
133+
</div>
134+
);
135+
}
136+
137+
/**
138+
* Triggered when a change occurs in the document awareness, to build again the users list.
139+
*/
140+
private _awarenessChange = () => {
141+
const clients = this._model?.sharedModel.awareness.getStates() as Map<
142+
number,
143+
User.IIdentity
144+
>;
145+
146+
const users: UsersItem.IUserData[] = [];
147+
if (clients) {
148+
clients.forEach((val, key) => {
149+
if (val.user) {
150+
users.push({ userId: key, userData: val.user as User.IIdentity });
151+
}
152+
});
153+
}
154+
this.setState(old => ({ ...old, usersList: users }));
155+
};
156+
157+
private _model: DocumentRegistry.IModel | null;
158+
private _iconRenderer:
159+
| ((props: UsersItem.IIconRendererProps) => JSX.Element)
160+
| null;
161+
}
162+
163+
/**
164+
* Default renderer for the user icon.
165+
*/
166+
export function DefaultIconRenderer(
167+
props: UsersItem.IIconRendererProps
168+
): JSX.Element {
169+
let el: JSX.Element;
170+
171+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
172+
const { user, model, ...htmlProps } = props;
173+
174+
const iconClasses = classes('lm-MenuBar-itemIcon', props.className || '');
175+
if (user.userData.avatar_url) {
176+
el = (
177+
<div
178+
{...htmlProps}
179+
key={user.userId}
180+
title={user.userData.display_name}
181+
className={classes(iconClasses, 'jp-MenuBar-imageIcon')}
182+
>
183+
<img src={user.userData.avatar_url} alt="" />
184+
</div>
185+
);
186+
} else {
187+
el = (
188+
<div
189+
{...htmlProps}
190+
key={user.userId}
191+
title={user.userData.display_name}
192+
className={classes(iconClasses, 'jp-MenuBar-anonymousIcon')}
193+
style={{ backgroundColor: user.userData.color }}
194+
>
195+
<span>{user.userData.initials}</span>
196+
</div>
197+
);
198+
}
199+
200+
return el;
201+
}

packages/collaboration/style/base.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
@import url('./menu.css');
77
@import url('./sidepanel.css');
8+
@import url('./users-item.css');
89

910
.jp-shared-link-body {
1011
user-select: none;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* -----------------------------------------------------------------------------
2+
| Copyright (c) Jupyter Development Team.
3+
| Distributed under the terms of the Modified BSD License.
4+
|---------------------------------------------------------------------------- */
5+
6+
.jp-toolbar-users-item {
7+
flex-grow: 1;
8+
display: flex;
9+
flex-direction: row;
10+
}
11+
12+
.jp-toolbar-users-item .jp-MenuBar-anonymousIcon,
13+
.jp-toolbar-users-item .jp-MenuBar-imageIcon {
14+
position: relative;
15+
left: 0;
16+
height: 22px;
17+
width: 22px;
18+
box-sizing: border-box;
19+
cursor: default;
20+
}

packages/collaborative-drive/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"watch": "tsc -b --watch"
3838
},
3939
"dependencies": {
40-
"@jupyter/ydoc": "^2.0.0 || ^3.0.0",
40+
"@jupyter/ydoc": "^2.1.3 || ^3.0.0",
4141
"@jupyterlab/services": "^7.2.0",
4242
"@lumino/coreutils": "^2.1.0",
4343
"@lumino/disposable": "^2.1.0"

packages/docprovider-extension/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"dependencies": {
5656
"@jupyter/collaborative-drive": "^3.1.0",
5757
"@jupyter/docprovider": "^3.1.0",
58-
"@jupyter/ydoc": "^2.0.0 || ^3.0.0",
58+
"@jupyter/ydoc": "^2.1.3 || ^3.0.0",
5959
"@jupyterlab/application": "^4.2.0",
6060
"@jupyterlab/apputils": "^4.2.0",
6161
"@jupyterlab/docregistry": "^4.2.0",

packages/docprovider/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
},
4343
"dependencies": {
4444
"@jupyter/collaborative-drive": "^3.1.0",
45-
"@jupyter/ydoc": "^2.0.0 || ^3.0.0",
45+
"@jupyter/ydoc": "^2.1.3 || ^3.0.0",
4646
"@jupyterlab/apputils": "^4.2.0",
4747
"@jupyterlab/cells": "^4.2.0",
4848
"@jupyterlab/coreutils": "^6.2.0",

yarn.lock

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2023,7 +2023,7 @@ __metadata:
20232023
"@jupyter/collaboration": ^3.1.0
20242024
"@jupyter/collaborative-drive": ^3.1.0
20252025
"@jupyter/docprovider": ^3.1.0
2026-
"@jupyter/ydoc": ^2.0.0 || ^3.0.0
2026+
"@jupyter/ydoc": ^2.1.3 || ^3.0.0
20272027
"@jupyterlab/application": ^4.2.0
20282028
"@jupyterlab/apputils": ^4.2.0
20292029
"@jupyterlab/builder": ^4.0.5
@@ -2072,7 +2072,7 @@ __metadata:
20722072
version: 0.0.0-use.local
20732073
resolution: "@jupyter/collaborative-drive@workspace:packages/collaborative-drive"
20742074
dependencies:
2075-
"@jupyter/ydoc": ^2.0.0 || ^3.0.0
2075+
"@jupyter/ydoc": ^2.1.3 || ^3.0.0
20762076
"@jupyterlab/services": ^7.2.0
20772077
"@lumino/coreutils": ^2.1.0
20782078
"@lumino/disposable": ^2.1.0
@@ -2087,7 +2087,7 @@ __metadata:
20872087
dependencies:
20882088
"@jupyter/collaborative-drive": ^3.1.0
20892089
"@jupyter/docprovider": ^3.1.0
2090-
"@jupyter/ydoc": ^2.0.0 || ^3.0.0
2090+
"@jupyter/ydoc": ^2.1.3 || ^3.0.0
20912091
"@jupyterlab/application": ^4.2.0
20922092
"@jupyterlab/apputils": ^4.2.0
20932093
"@jupyterlab/builder": ^4.0.0
@@ -2114,7 +2114,7 @@ __metadata:
21142114
resolution: "@jupyter/docprovider@workspace:packages/docprovider"
21152115
dependencies:
21162116
"@jupyter/collaborative-drive": ^3.1.0
2117-
"@jupyter/ydoc": ^2.0.0 || ^3.0.0
2117+
"@jupyter/ydoc": ^2.1.3 || ^3.0.0
21182118
"@jupyterlab/apputils": ^4.2.0
21192119
"@jupyterlab/cells": ^4.2.0
21202120
"@jupyterlab/coreutils": ^6.2.0
@@ -2181,17 +2181,17 @@ __metadata:
21812181
languageName: node
21822182
linkType: hard
21832183

2184-
"@jupyter/ydoc@npm:^2.0.0 || ^3.0.0, @jupyter/ydoc@npm:^3.0.0":
2185-
version: 3.0.0
2186-
resolution: "@jupyter/ydoc@npm:3.0.0"
2184+
"@jupyter/ydoc@npm:^2.1.3 || ^3.0.0, @jupyter/ydoc@npm:^3.0.0":
2185+
version: 3.0.2
2186+
resolution: "@jupyter/ydoc@npm:3.0.2"
21872187
dependencies:
21882188
"@jupyterlab/nbformat": ^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0
21892189
"@lumino/coreutils": ^1.11.0 || ^2.0.0
21902190
"@lumino/disposable": ^1.10.0 || ^2.0.0
21912191
"@lumino/signaling": ^1.10.0 || ^2.0.0
21922192
y-protocols: ^1.0.5
21932193
yjs: ^13.5.40
2194-
checksum: e9419a461f33d2685db346b19806865fe37f61b2ca33eb39c4ea905d765794a928442adf1bbffda67b665bdeba3be9a082189a57eaab5367aeaf6b57caeda822
2194+
checksum: 770f73459635c74bd0e5cacdca1ea1f77ee8efd6e7cd58f0ccbb167ae8374e73118620f4f3628646281160a7bc7389f374bd2106f1e799bdc8f78cad0ce05b28
21952195
languageName: node
21962196
linkType: hard
21972197

0 commit comments

Comments
 (0)