Skip to content

Commit 85223cd

Browse files
committed
Add the custom notification and component to the writers component
1 parent 7a906eb commit 85223cd

File tree

5 files changed

+168
-13
lines changed

5 files changed

+168
-13
lines changed

packages/jupyter-chat/src/components/chat.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
InputToolbarRegistry
1818
} from './input';
1919
import { JlThemeProvider } from './jl-theme-provider';
20-
import { ChatMessages } from './messages';
20+
import { ChatMessages, WriterComponent } from './messages';
2121
import { AttachmentOpenerContext } from '../context';
2222
import { IChatModel } from '../model';
2323
import {
@@ -42,6 +42,7 @@ export function ChatBody(props: Chat.IChatBodyProps): JSX.Element {
4242
inputToolbarRegistry={inputToolbarRegistry}
4343
messageFooterRegistry={props.messageFooterRegistry}
4444
welcomeMessage={props.welcomeMessage}
45+
writerComponent={props.writerComponent}
4546
/>
4647
<ChatInput
4748
sx={{
@@ -141,6 +142,10 @@ export namespace Chat {
141142
* The welcome message.
142143
*/
143144
welcomeMessage?: string;
145+
/**
146+
* The typing notification widget.
147+
*/
148+
writerComponent?: WriterComponent;
144149
}
145150

146151
/**

packages/jupyter-chat/src/components/messages/messages.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ import { ChatMessageHeader } from './header';
1414
import { ChatMessage } from './message';
1515
import { Navigation } from './navigation';
1616
import { WelcomeMessage } from './welcome';
17-
import { WritingUsersList } from './writers';
17+
import { WriterComponent, WritingUsersList } from './writers';
1818
import { IInputToolbarRegistry } from '../input';
1919
import { ScrollContainer } from '../scroll-container';
2020
import { IChatCommandRegistry, IMessageFooterRegistry } from '../../registers';
2121
import { IChatModel } from '../../model';
22-
import { IChatMessage, IUser } from '../../types';
22+
import { IChatMessage } from '../../types';
2323

2424
const MESSAGES_BOX_CLASS = 'jp-chat-messages-container';
2525
const MESSAGE_CLASS = 'jp-chat-message';
@@ -53,6 +53,10 @@ export type BaseMessageProps = {
5353
* The welcome message.
5454
*/
5555
welcomeMessage?: string;
56+
/**
57+
* The typing notification widget.
58+
*/
59+
writerComponent?: WriterComponent;
5660
};
5761

5862
/**
@@ -62,7 +66,9 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
6266
const { model } = props;
6367
const [messages, setMessages] = useState<IChatMessage[]>(model.messages);
6468
const refMsgBox = useRef<HTMLDivElement>(null);
65-
const [currentWriters, setCurrentWriters] = useState<IUser[]>([]);
69+
const [currentWriters, setCurrentWriters] = useState<IChatModel.IWriter[]>(
70+
[]
71+
);
6672
const [allRendered, setAllRendered] = useState<boolean>(false);
6773

6874
// The list of message DOM and their rendered promises.
@@ -96,7 +102,7 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
96102
}
97103

98104
function handleWritersChange(_: IChatModel, writers: IChatModel.IWriter[]) {
99-
setCurrentWriters(writers.map(writer => writer.user));
105+
setCurrentWriters([...writers]);
100106
}
101107

102108
model.messagesUpdated.connect(handleChatEvents);
@@ -211,7 +217,10 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
211217
})}
212218
</Box>
213219
</ScrollContainer>
214-
<WritingUsersList writers={currentWriters}></WritingUsersList>
220+
<WritingUsersList
221+
writers={currentWriters}
222+
writerComponent={props.writerComponent}
223+
></WritingUsersList>
215224
<Navigation {...props} refMsgBox={refMsgBox} allRendered={allRendered} />
216225
</>
217226
);

packages/jupyter-chat/src/components/messages/writers.tsx

Lines changed: 136 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ import { Box, Typography } from '@mui/material';
77
import React, { useMemo } from 'react';
88

99
import { Avatar } from '../avatar';
10+
import { IChatModel } from '../../model';
11+
import { ISignal, Signal } from '@lumino/signaling';
1012
import { IUser } from '../../types';
1113

1214
const WRITERS_CLASS = 'jp-chat-writers';
15+
const WRITERS_ROW_CLASS = 'jp-chat-writers-row';
16+
17+
const DEFAULT_TEXT = 'is writing';
1318

1419
/**
1520
* The writers component props.
@@ -18,7 +23,11 @@ type writersProps = {
1823
/**
1924
* The list of users currently writing.
2025
*/
21-
writers: IUser[];
26+
writers: IChatModel.IWriter[];
27+
/**
28+
* The component to render next to the writers.
29+
*/
30+
writerComponent?: WriterComponent;
2231
};
2332

2433
/**
@@ -36,14 +45,53 @@ const TypingIndicator = (): JSX.Element => (
3645
* The writers component, displaying the current writers.
3746
*/
3847
export function WritingUsersList(props: writersProps): JSX.Element | null {
48+
const { writers, writerComponent } = props;
49+
50+
// Don't render if no writers
51+
if (writers.length === 0) {
52+
return null;
53+
}
54+
55+
// Default rendering for users without custom typing indicator and if there is no
56+
// component to add to the writing notification.
57+
const defaultWriters = writerComponent?.component
58+
? []
59+
: writers.filter(writer => !writer.typingIndicator);
60+
const defaultWritersComponent = defaultWritingUsers({
61+
writers: defaultWriters.map(writer => writer.user)
62+
});
63+
64+
// Custom rendering for users with custom typing indicator or if there is a component
65+
// to add to the writing notification.
66+
const customWriters = writerComponent?.component
67+
? writers
68+
: writers.filter(writer => writer.typingIndicator);
69+
const customWritersComponent = customWritingUser({
70+
writers: customWriters,
71+
writerComponent: writerComponent?.component
72+
});
73+
74+
return (
75+
<Box className={WRITERS_CLASS}>
76+
{defaultWritersComponent !== null && defaultWritersComponent}
77+
{customWritersComponent !== null && customWritersComponent}
78+
</Box>
79+
);
80+
}
81+
82+
/**
83+
* The default rendering of writing users, all in a row.
84+
* This renderer is used if there is no custom component and no custom typing indicator.
85+
*/
86+
function defaultWritingUsers(props: { writers: IUser[] }): JSX.Element | null {
3987
const { writers } = props;
4088

4189
// Don't render if no writers
4290
if (writers.length === 0) {
4391
return null;
4492
}
4593

46-
const writersText = writers.length > 1 ? ' are writing' : ' is writing';
94+
const writersText = writers.length > 1 ? 'are writing' : DEFAULT_TEXT;
4795

4896
const writingUsers: JSX.Element[] = useMemo(
4997
() =>
@@ -66,16 +114,100 @@ export function WritingUsersList(props: writersProps): JSX.Element | null {
66114
);
67115

68116
return (
69-
<Box className={`${WRITERS_CLASS}`}>
117+
<Box className={`${WRITERS_ROW_CLASS}`}>
70118
<Box className="jp-chat-writers-content">
71119
{writingUsers}
72120
<Box className="jp-chat-writing-status">
73121
<Typography variant="body2" className="jp-chat-writing-text">
74-
{writersText}
122+
{` ${writersText}`}
75123
</Typography>
76124
<TypingIndicator />
77125
</Box>
78126
</Box>
79127
</Box>
80128
);
81129
}
130+
131+
/**
132+
* The custom rendering of writing users, one per row.
133+
* This renderer is used if there is a custom component or a custom typing indicator.
134+
*/
135+
function customWritingUser(props: {
136+
writers: IChatModel.IWriter[];
137+
writerComponent?: React.FC<WriterComponentProps>;
138+
}): JSX.Element | null {
139+
const { writers } = props;
140+
141+
// Don't render if no writers
142+
if (writers.length === 0) {
143+
return null;
144+
}
145+
146+
const writingUsers: JSX.Element[] = writers.map(writer => {
147+
const username =
148+
writer.user.display_name ??
149+
writer.user.name ??
150+
(writer.user.username || 'User undefined');
151+
152+
const writerText = writer.typingIndicator ?? DEFAULT_TEXT;
153+
return (
154+
<Box key={writer.user.username} className="jp-chat-writer-item">
155+
<Avatar user={writer.user} small />
156+
<Typography variant="body2" className="jp-chat-writer-name">
157+
{username}
158+
</Typography>
159+
<Box className="jp-chat-writing-status">
160+
<Typography variant="body2" className="jp-chat-writing-text">
161+
{` ${writerText}`}
162+
</Typography>
163+
<TypingIndicator />
164+
</Box>
165+
{props.writerComponent && <props.writerComponent writer={writer} />}
166+
</Box>
167+
);
168+
});
169+
170+
return (
171+
<>
172+
{writingUsers.map(writingUser => (
173+
<Box className={`${WRITERS_ROW_CLASS}`}>{writingUser}</Box>
174+
))}
175+
</>
176+
);
177+
}
178+
179+
export type WriterComponentProps = {
180+
/**
181+
* The writer associated to this component.
182+
*/
183+
writer: IChatModel.IWriter;
184+
};
185+
186+
export class WriterComponent {
187+
/**
188+
* The react component.
189+
*/
190+
get component(): React.FC<WriterComponentProps> | undefined {
191+
return this._component;
192+
}
193+
set component(value: React.FC<WriterComponentProps> | undefined) {
194+
this._component = value;
195+
this._changed.emit(this._component);
196+
}
197+
198+
/**
199+
* Emitting when the component changed.
200+
*/
201+
get changed(): ISignal<
202+
WriterComponent,
203+
React.FC<WriterComponentProps> | undefined
204+
> {
205+
return this._changed;
206+
}
207+
208+
private _component: React.FC<WriterComponentProps> | undefined;
209+
private _changed = new Signal<
210+
WriterComponent,
211+
React.FC<WriterComponentProps> | undefined
212+
>(this);
213+
}

packages/jupyter-chat/src/model.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -791,9 +791,13 @@ export namespace IChatModel {
791791
*/
792792
user: IUser;
793793
/**
794-
* The message ID (optional)
794+
* The message ID (optional).
795795
*/
796796
messageID?: string;
797+
/**
798+
* The writer typing indicator (optional)
799+
*/
800+
typingIndicator?: string;
797801
}
798802
}
799803

packages/jupyter-chat/style/chat.css

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,12 @@
7474
margin-top: 0;
7575
}
7676

77-
.jp-chat-writers {
77+
.jp-chat-writer {
78+
display: flex;
79+
flex-direction: column;
80+
}
81+
82+
.jp-chat-writers-row {
7883
display: flex;
7984
flex-wrap: wrap;
8085
position: sticky;
@@ -160,7 +165,7 @@
160165
}
161166
}
162167

163-
.jp-chat-writers > div {
168+
.jp-chat-writers-row > div {
164169
display: flex;
165170
align-items: center;
166171
gap: 0.2em;

0 commit comments

Comments
 (0)