@@ -10,7 +10,7 @@ import {
10
10
caretDownEmptyIcon ,
11
11
classes
12
12
} from '@jupyterlab/ui-components' ;
13
- import { Avatar , Box , Typography } from '@mui/material' ;
13
+ import { Avatar as MuiAvatar , Box , Typography } from '@mui/material' ;
14
14
import type { SxProps , Theme } from '@mui/material' ;
15
15
import clsx from 'clsx' ;
16
16
import React , { useEffect , useState , useRef } from 'react' ;
@@ -19,13 +19,14 @@ import { ChatInput } from './chat-input';
19
19
import { RendermimeMarkdown } from './rendermime-markdown' ;
20
20
import { ScrollContainer } from './scroll-container' ;
21
21
import { IChatModel } from '../model' ;
22
- import { IChatMessage } from '../types' ;
22
+ import { IChatMessage , IUser } from '../types' ;
23
23
24
24
const MESSAGES_BOX_CLASS = 'jp-chat-messages-container' ;
25
25
const MESSAGE_CLASS = 'jp-chat-message' ;
26
26
const MESSAGE_STACKED_CLASS = 'jp-chat-message-stacked' ;
27
27
const MESSAGE_HEADER_CLASS = 'jp-chat-message-header' ;
28
28
const MESSAGE_TIME_CLASS = 'jp-chat-message-time' ;
29
+ const WRITERS_CLASS = 'jp-chat-writers' ;
29
30
const NAVIGATION_BUTTON_CLASS = 'jp-chat-navigation' ;
30
31
const NAVIGATION_UNREAD_CLASS = 'jp-chat-navigation-unread' ;
31
32
const NAVIGATION_TOP_CLASS = 'jp-chat-navigation-top' ;
@@ -47,6 +48,7 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
47
48
const [ messages , setMessages ] = useState < IChatMessage [ ] > ( model . messages ) ;
48
49
const refMsgBox = useRef < HTMLDivElement > ( null ) ;
49
50
const inViewport = useRef < number [ ] > ( [ ] ) ;
51
+ const [ currentWriters , setCurrentWriters ] = useState < IUser [ ] > ( [ ] ) ;
50
52
51
53
// The intersection observer that listen to all the message visibility.
52
54
const observerRef = useRef < IntersectionObserver > (
@@ -68,6 +70,7 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
68
70
}
69
71
70
72
fetchHistory ( ) ;
73
+ setCurrentWriters ( [ ] ) ;
71
74
} , [ model ] ) ;
72
75
73
76
/**
@@ -78,9 +81,16 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
78
81
setMessages ( [ ...model . messages ] ) ;
79
82
}
80
83
84
+ function handleWritersChange ( _ : IChatModel , writers : IUser [ ] ) {
85
+ setCurrentWriters ( writers ) ;
86
+ }
87
+
81
88
model . messagesUpdated . connect ( handleChatEvents ) ;
89
+ model . writersChanged ?. connect ( handleWritersChange ) ;
90
+
82
91
return function cleanup ( ) {
83
92
model . messagesUpdated . disconnect ( handleChatEvents ) ;
93
+ model . writersChanged ?. disconnect ( handleChatEvents ) ;
84
94
} ;
85
95
} , [ model ] ) ;
86
96
@@ -144,6 +154,7 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
144
154
) ;
145
155
} ) }
146
156
</ Box >
157
+ < Writers writers = { currentWriters } > </ Writers >
147
158
</ ScrollContainer >
148
159
< Navigation { ...props } refMsgBox = { refMsgBox } />
149
160
</ >
@@ -163,10 +174,6 @@ type ChatMessageHeaderProps = {
163
174
*/
164
175
export function ChatMessageHeader ( props : ChatMessageHeaderProps ) : JSX . Element {
165
176
const [ datetime , setDatetime ] = useState < Record < number , string > > ( { } ) ;
166
- const sharedStyles : SxProps < Theme > = {
167
- height : '24px' ,
168
- width : '24px'
169
- } ;
170
177
const message = props . message ;
171
178
const sender = message . sender ;
172
179
/**
@@ -206,32 +213,7 @@ export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
206
213
}
207
214
} ) ;
208
215
209
- const bgcolor = sender . color ;
210
- const avatar = message . stacked ? null : sender . avatar_url ? (
211
- < Avatar
212
- sx = { {
213
- ...sharedStyles ,
214
- ...( bgcolor && { bgcolor } )
215
- } }
216
- src = { sender . avatar_url }
217
- > </ Avatar >
218
- ) : sender . initials ? (
219
- < Avatar
220
- sx = { {
221
- ...sharedStyles ,
222
- ...( bgcolor && { bgcolor } )
223
- } }
224
- >
225
- < Typography
226
- sx = { {
227
- fontSize : 'var(--jp-ui-font-size1)' ,
228
- color : 'var(--jp-ui-inverse-font-color1)'
229
- } }
230
- >
231
- { sender . initials }
232
- </ Typography >
233
- </ Avatar >
234
- ) : null ;
216
+ const avatar = message . stacked ? null : Avatar ( { user : sender } ) ;
235
217
236
218
const name =
237
219
sender . display_name ?? sender . name ?? ( sender . username || 'User undefined' ) ;
@@ -408,6 +390,45 @@ export function ChatMessage(props: ChatMessageProps): JSX.Element {
408
390
) ;
409
391
}
410
392
393
+ /**
394
+ * The writers component props.
395
+ */
396
+ type writersProps = {
397
+ /**
398
+ * The list of users currently writing.
399
+ */
400
+ writers : IUser [ ] ;
401
+ } ;
402
+
403
+ /**
404
+ * The writers component, displaying the current writers.
405
+ */
406
+ export function Writers ( props : writersProps ) : JSX . Element | null {
407
+ const { writers } = props ;
408
+ return writers . length > 0 ? (
409
+ < Box className = { WRITERS_CLASS } >
410
+ { writers . map ( ( writer , index ) => (
411
+ < div >
412
+ < Avatar user = { writer } small />
413
+ < span >
414
+ { writer . display_name ??
415
+ writer . name ??
416
+ ( writer . username || 'User undefined' ) }
417
+ </ span >
418
+ < span >
419
+ { index < writers . length - 1
420
+ ? index < writers . length - 2
421
+ ? ', '
422
+ : ' and '
423
+ : '' }
424
+ </ span >
425
+ </ div >
426
+ ) ) }
427
+ < span > { ( writers . length > 1 ? ' are' : ' is' ) + ' writing' } </ span >
428
+ </ Box >
429
+ ) : null ;
430
+ }
431
+
411
432
/**
412
433
* The navigation component props.
413
434
*/
@@ -544,3 +565,55 @@ export function Navigation(props: NavigationProps): JSX.Element {
544
565
</ >
545
566
) ;
546
567
}
568
+
569
+ /**
570
+ * The avatar props.
571
+ */
572
+ type AvatarProps = {
573
+ /**
574
+ * The user to display an avatar.
575
+ */
576
+ user : IUser ;
577
+ /**
578
+ * Whether the avatar should be small.
579
+ */
580
+ small ?: boolean ;
581
+ } ;
582
+
583
+ /**
584
+ * the avatar component.
585
+ */
586
+ export function Avatar ( props : AvatarProps ) : JSX . Element | null {
587
+ const { user } = props ;
588
+
589
+ const sharedStyles : SxProps < Theme > = {
590
+ height : `${ props . small ? '16' : '24' } px` ,
591
+ width : `${ props . small ? '16' : '24' } px` ,
592
+ bgcolor : user . color ,
593
+ fontSize : `var(--jp-ui-font-size${ props . small ? '0' : '1' } )`
594
+ } ;
595
+
596
+ return user . avatar_url ? (
597
+ < MuiAvatar
598
+ sx = { {
599
+ ...sharedStyles
600
+ } }
601
+ src = { user . avatar_url }
602
+ > </ MuiAvatar >
603
+ ) : user . initials ? (
604
+ < MuiAvatar
605
+ sx = { {
606
+ ...sharedStyles
607
+ } }
608
+ >
609
+ < Typography
610
+ sx = { {
611
+ fontSize : `var(--jp-ui-font-size${ props . small ? '0' : '1' } )` ,
612
+ color : 'var(--jp-ui-inverse-font-color1)'
613
+ } }
614
+ >
615
+ { user . initials }
616
+ </ Typography >
617
+ </ MuiAvatar >
618
+ ) : null ;
619
+ }
0 commit comments