Skip to content

Commit 85efd5c

Browse files
brichetandrii-i
authored andcommitted
Add the list of opened shared documents to the user awareness (jupyterlab#287)
* Add the list of opened shared documents to the user awareness * update yarn.lock * Modify the collaborators panel to display the list of opened shared documents * lint * Uses the style of the filebrowser for the list of opened documents * Adds a colapser caret and overlay to the filename * Update and add tests * Clean sources * Harmonize jupyterlab dependencies
1 parent af6094c commit 85efd5c

File tree

13 files changed

+302
-96
lines changed

13 files changed

+302
-96
lines changed

packages/collaboration-extension/src/collaboration.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ export const rtcGlobalAwarenessPlugin: JupyterFrontEndPlugin<IAwareness> = {
109109

110110
state.changed.connect(async () => {
111111
const data: any = await state.toJSON();
112-
const current = data['layout-restorer:data']?.main?.current || '';
112+
const current: string = data['layout-restorer:data']?.main?.current || '';
113113

114-
if (current.startsWith('editor') || current.startsWith('notebook')) {
114+
if (current.match(/^\w+:RTC:/)) {
115115
awareness.setLocalStateField('current', current);
116116
} else {
117117
awareness.setLocalStateField('current', null);
@@ -161,7 +161,8 @@ export const rtcPanelPlugin: JupyterFrontEndPlugin<void> = {
161161
const collaboratorsPanel = new CollaboratorsPanel(
162162
user,
163163
awareness,
164-
fileopener
164+
fileopener,
165+
app.docRegistry
165166
);
166167
collaboratorsPanel.title.label = trans.__('Online Collaborators');
167168
userPanel.addWidget(collaboratorsPanel);

packages/collaboration/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,11 @@
4444
"@jupyter/docprovider": "^3.0.0-alpha.2",
4545
"@jupyterlab/apputils": "^4.0.5",
4646
"@jupyterlab/coreutils": "^6.0.5",
47+
"@jupyterlab/docregistry": "^4.0.5",
4748
"@jupyterlab/services": "^7.0.5",
4849
"@jupyterlab/ui-components": "^4.0.5",
4950
"@lumino/coreutils": "^2.1.0",
51+
"@lumino/signaling": "^2.0.0",
5052
"@lumino/virtualdom": "^2.0.0",
5153
"@lumino/widgets": "^2.1.0",
5254
"react": "^18.2.0",
Lines changed: 174 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
// Copyright (c) Jupyter Development Team.
22
// Distributed under the terms of the Modified BSD License.
33

4-
import * as React from 'react';
4+
import { ReactWidget } from '@jupyterlab/apputils';
55

6-
import { Awareness } from 'y-protocols/awareness';
6+
import { DocumentRegistry } from '@jupyterlab/docregistry';
77

8-
import { Panel } from '@lumino/widgets';
8+
import { User } from '@jupyterlab/services';
99

10-
import { ReactWidget } from '@jupyterlab/apputils';
10+
import { LabIcon, caretDownIcon, fileIcon } from '@jupyterlab/ui-components';
1111

12-
import { User } from '@jupyterlab/services';
12+
import { Signal, ISignal } from '@lumino/signaling';
1313

14-
import { PathExt } from '@jupyterlab/coreutils';
14+
import { Panel } from '@lumino/widgets';
15+
16+
import React, { useState } from 'react';
17+
18+
import { Awareness } from 'y-protocols/awareness';
1519

1620
import { ICollaboratorAwareness } from './tokens';
1721

@@ -31,7 +35,17 @@ const COLLABORATORS_LIST_CLASS = 'jp-CollaboratorsList';
3135
const COLLABORATOR_CLASS = 'jp-Collaborator';
3236

3337
/**
34-
* The CSS class added to each collaborator element.
38+
* The CSS class added to each collaborator header.
39+
*/
40+
const COLLABORATOR_HEADER_CLASS = 'jp-CollaboratorHeader';
41+
42+
/**
43+
* The CSS class added to each collaborator header collapser.
44+
*/
45+
const COLLABORATOR_HEADER_COLLAPSER_CLASS = 'jp-CollaboratorHeaderCollapser';
46+
47+
/**
48+
* The CSS class added to each collaborator header with document.
3549
*/
3650
const CLICKABLE_COLLABORATOR_CLASS = 'jp-ClickableCollaborator';
3751

@@ -40,15 +54,22 @@ const CLICKABLE_COLLABORATOR_CLASS = 'jp-ClickableCollaborator';
4054
*/
4155
const COLLABORATOR_ICON_CLASS = 'jp-CollaboratorIcon';
4256

43-
export class CollaboratorsPanel extends Panel {
44-
private _currentUser: User.IManager;
45-
private _awareness: Awareness;
46-
private _body: CollaboratorsBody;
57+
/**
58+
* The CSS class added to the files list.
59+
*/
60+
const COLLABORATOR_FILES_CLASS = 'jp-CollaboratorFiles';
4761

62+
/**
63+
* The CSS class added to the files in the list.
64+
*/
65+
const COLLABORATOR_FILE_CLASS = 'jp-CollaboratorFile';
66+
67+
export class CollaboratorsPanel extends Panel {
4868
constructor(
4969
currentUser: User.IManager,
5070
awareness: Awareness,
51-
fileopener: (path: string) => void
71+
fileopener: (path: string) => void,
72+
docRegistry?: DocumentRegistry
5273
) {
5374
super({});
5475

@@ -58,9 +79,15 @@ export class CollaboratorsPanel extends Panel {
5879

5980
this.addClass(COLLABORATORS_PANEL_CLASS);
6081

61-
this._body = new CollaboratorsBody(fileopener);
62-
this.addWidget(this._body);
63-
this.update();
82+
this.addWidget(
83+
ReactWidget.create(
84+
<CollaboratorsBody
85+
fileopener={fileopener}
86+
collaboratorsChanged={this._collaboratorsChanged}
87+
docRegistry={docRegistry}
88+
></CollaboratorsBody>
89+
)
90+
);
6491

6592
this._awareness.on('change', this._onAwarenessChanged);
6693
}
@@ -80,78 +107,148 @@ export class CollaboratorsPanel extends Panel {
80107
collaborators.push(value);
81108
}
82109
});
83-
84-
this._body.collaborators = collaborators;
110+
this._collaboratorsChanged.emit(collaborators);
85111
};
112+
private _currentUser: User.IManager;
113+
private _awareness: Awareness;
114+
private _collaboratorsChanged = new Signal<this, ICollaboratorAwareness[]>(
115+
this
116+
);
86117
}
87118

88-
/**
89-
* The collaborators list.
90-
*/
91-
export class CollaboratorsBody extends ReactWidget {
92-
private _collaborators: ICollaboratorAwareness[] = [];
93-
private _fileopener: (path: string) => void;
94-
95-
constructor(fileopener: (path: string) => void) {
96-
super();
97-
this._fileopener = fileopener;
98-
this.addClass(COLLABORATORS_LIST_CLASS);
99-
}
100-
101-
get collaborators(): ICollaboratorAwareness[] {
102-
return this._collaborators;
103-
}
119+
export function CollaboratorsBody(props: {
120+
collaboratorsChanged: ISignal<CollaboratorsPanel, ICollaboratorAwareness[]>;
121+
fileopener: (path: string) => void;
122+
docRegistry?: DocumentRegistry;
123+
}): JSX.Element {
124+
const [collaborators, setCollaborators] = useState<ICollaboratorAwareness[]>(
125+
[]
126+
);
127+
128+
props.collaboratorsChanged.connect((_, value) => {
129+
setCollaborators(value);
130+
});
131+
132+
return (
133+
<div className={COLLABORATORS_LIST_CLASS}>
134+
{collaborators.map((collaborator, i) => {
135+
return (
136+
<Collaborator
137+
collaborator={collaborator}
138+
fileopener={props.fileopener}
139+
docRegistry={props.docRegistry}
140+
></Collaborator>
141+
);
142+
})}
143+
</div>
144+
);
145+
}
104146

105-
set collaborators(value: ICollaboratorAwareness[]) {
106-
this._collaborators = value;
107-
this.update();
147+
export function Collaborator(props: {
148+
collaborator: ICollaboratorAwareness;
149+
fileopener: (path: string) => void;
150+
docRegistry?: DocumentRegistry;
151+
}): JSX.Element {
152+
const [open, setOpen] = useState<boolean>(false);
153+
const { collaborator, fileopener } = props;
154+
let currentMain = '';
155+
156+
if (collaborator.current) {
157+
const path = collaborator.current.split(':');
158+
currentMain = `${path[1]}:${path[2]}`;
108159
}
109160

110-
render(): React.ReactElement<any>[] {
111-
return this._collaborators.map((value, i) => {
112-
let canOpenCurrent = false;
113-
let current = '';
114-
let separator = '';
115-
let currentFileLocation = '';
116-
117-
if (value.current) {
118-
canOpenCurrent = true;
119-
const path = value.current.split(':');
120-
currentFileLocation = `${path[1]}:${path[2]}`;
121-
122-
current = PathExt.basename(path[2]);
123-
current =
124-
current.length > 25 ? current.slice(0, 12).concat('…') : current;
125-
separator = '•';
126-
}
161+
const documents: string[] = collaborator.documents || [];
162+
163+
const docs = documents.map(document => {
164+
const path = document.split(':');
165+
const fileTypes = props.docRegistry
166+
?.getFileTypesForPath(path[1])
167+
?.filter(ft => ft.icon !== undefined);
168+
const icon = fileTypes ? fileTypes[0].icon! : fileIcon;
169+
const iconClass: string | undefined = fileTypes
170+
? fileTypes[0].iconClass
171+
: undefined;
172+
173+
return {
174+
filepath: path[1],
175+
filename:
176+
path[1].length > 40
177+
? path[1]
178+
.slice(0, 10)
179+
.concat('…')
180+
.concat(path[1].slice(path[1].length - 15))
181+
: path[1],
182+
fileLocation: document,
183+
icon,
184+
iconClass
185+
};
186+
});
187+
188+
const onClick = () => {
189+
if (docs.length) {
190+
setOpen(!open);
191+
}
192+
};
127193

128-
const onClick = () => {
129-
if (canOpenCurrent) {
130-
this._fileopener(currentFileLocation);
194+
return (
195+
<div className={COLLABORATOR_CLASS}>
196+
<div
197+
className={
198+
docs.length
199+
? `${CLICKABLE_COLLABORATOR_CLASS} ${COLLABORATOR_HEADER_CLASS}`
200+
: COLLABORATOR_HEADER_CLASS
131201
}
132-
};
133-
134-
const displayName = `${value.user.display_name} ${separator} ${current}`;
135-
136-
return (
137-
<div
202+
onClick={documents ? onClick : undefined}
203+
>
204+
<LabIcon.resolveReact
205+
icon={caretDownIcon}
138206
className={
139-
canOpenCurrent
140-
? `${CLICKABLE_COLLABORATOR_CLASS} ${COLLABORATOR_CLASS}`
141-
: COLLABORATOR_CLASS
207+
COLLABORATOR_HEADER_COLLAPSER_CLASS +
208+
(open ? ' jp-mod-expanded' : '')
142209
}
143-
key={i}
144-
onClick={onClick}
210+
tag={'div'}
211+
/>
212+
<div
213+
className={COLLABORATOR_ICON_CLASS}
214+
style={{ backgroundColor: collaborator.user.color }}
145215
>
146-
<div
147-
className={COLLABORATOR_ICON_CLASS}
148-
style={{ backgroundColor: value.user.color }}
149-
>
150-
<span>{value.user.initials}</span>
151-
</div>
152-
<span>{displayName}</span>
216+
<span>{collaborator.user.initials}</span>
153217
</div>
154-
);
155-
});
156-
}
218+
<span>{collaborator.user.display_name}</span>
219+
</div>
220+
<div
221+
className={`${COLLABORATOR_FILES_CLASS} jp-DirListing`}
222+
style={open ? {} : { display: 'none' }}
223+
>
224+
<ul className={'jp-DirListing-content'}>
225+
{docs.map(doc => {
226+
return (
227+
<li
228+
className={
229+
'jp-DirListing-item ' +
230+
(doc.fileLocation === currentMain
231+
? `${COLLABORATOR_FILE_CLASS} jp-mod-running`
232+
: COLLABORATOR_FILE_CLASS)
233+
}
234+
key={doc.filename}
235+
onClick={() => fileopener(doc.fileLocation)}
236+
>
237+
<LabIcon.resolveReact
238+
icon={doc.icon}
239+
iconClass={doc.iconClass}
240+
tag={'span'}
241+
className={'jp-DirListing-itemIcon'}
242+
stylesheet={'listing'}
243+
/>
244+
<span className={'jp-DirListing-itemText'} title={doc.filepath}>
245+
{doc.filename}
246+
</span>
247+
</li>
248+
);
249+
})}
250+
</ul>
251+
</div>
252+
</div>
253+
);
157254
}

packages/collaboration/src/tokens.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,12 @@ export interface ICollaboratorAwareness {
8787
user: User.IIdentity;
8888

8989
/**
90-
* The current file/context the user is working on.
90+
* The current file/context the user is working on (current panel in main area).
9191
*/
9292
current?: string;
93+
94+
/**
95+
* The shared documents opened by the user.
96+
*/
97+
documents?: string[];
9398
}

0 commit comments

Comments
 (0)