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
1620import { ICollaboratorAwareness } from './tokens' ;
1721
@@ -31,7 +35,17 @@ const COLLABORATORS_LIST_CLASS = 'jp-CollaboratorsList';
3135const 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 */
3650const CLICKABLE_COLLABORATOR_CLASS = 'jp-ClickableCollaborator' ;
3751
@@ -40,15 +54,22 @@ const CLICKABLE_COLLABORATOR_CLASS = 'jp-ClickableCollaborator';
4054 */
4155const 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}
0 commit comments