Skip to content

Commit 8e91bbb

Browse files
authored
Messages as a list in collaborative chat document (#31)
* Store the list of message in the model instead of the widget, to easily manipulate it * Change the chat document to have a list of messages instead of a map * Fix collaborative chat tests * lint * Improve the way the changes on messages are handled
1 parent 0d21e56 commit 8e91bbb

File tree

9 files changed

+252
-257
lines changed

9 files changed

+252
-257
lines changed

packages/jupyter-chat/src/__tests__/model.spec.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
import { ChatModel, IChatModel } from '../model';
11-
import { IChatMessage, IMessage } from '../types';
11+
import { IChatMessage } from '../types';
1212

1313
describe('test chat model', () => {
1414
describe('model instantiation', () => {
@@ -33,7 +33,7 @@ describe('test chat model', () => {
3333
}
3434

3535
let model: IChatModel;
36-
let messages: IMessage[];
36+
let messages: IChatMessage[];
3737
const msg = {
3838
type: 'msg',
3939
id: 'message1',
@@ -48,22 +48,22 @@ describe('test chat model', () => {
4848

4949
it('should signal incoming message', () => {
5050
model = new ChatModel();
51-
model.incomingMessage.connect((sender: IChatModel, message: IMessage) => {
51+
model.messagesUpdated.connect((sender: IChatModel) => {
5252
expect(sender).toBe(model);
53-
messages.push(message);
53+
messages = model.messages;
5454
});
55-
model.onMessage(msg);
55+
model.messageAdded(msg);
5656
expect(messages).toHaveLength(1);
5757
expect(messages[0]).toBe(msg);
5858
});
5959

6060
it('should format message', () => {
6161
model = new TestChat();
62-
model.incomingMessage.connect((sender: IChatModel, message: IMessage) => {
62+
model.messagesUpdated.connect((sender: IChatModel) => {
6363
expect(sender).toBe(model);
64-
messages.push(message);
64+
messages = model.messages;
6565
});
66-
model.onMessage({ ...msg } as IChatMessage);
66+
model.messageAdded({ ...msg });
6767
expect(messages).toHaveLength(1);
6868
expect(messages[0]).not.toBe(msg);
6969
expect((messages[0] as IChatMessage).body).toBe('formatted msg');

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

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { ChatMessages } from './chat-messages';
1616
import { ChatInput } from './chat-input';
1717
import { ScrollContainer } from './scroll-container';
1818
import { IChatModel } from '../model';
19-
import { IChatMessage, IMessage } from '../types';
19+
import { IChatMessage } from '../types';
2020

2121
type ChatBodyProps = {
2222
model: IChatModel;
@@ -50,49 +50,13 @@ function ChatBody({
5050
* Effect: listen to chat messages
5151
*/
5252
useEffect(() => {
53-
function handleChatEvents(_: IChatModel, message: IMessage) {
54-
if (message.type === 'clear') {
55-
setMessages([]);
56-
return;
57-
} else {
58-
setMessages((messageGroups: IChatMessage[]) => {
59-
const existingMessages = [...messageGroups];
60-
61-
const messageIndex = existingMessages.findIndex(
62-
msg => msg.id === message.id
63-
);
64-
if (messageIndex > -1) {
65-
// The message is an update of an existing one (or a removal).
66-
// Let's remove it anyway (to avoid position conflict if timestamp has
67-
// changed) and add the new one if it is an update.
68-
existingMessages.splice(messageIndex, 1);
69-
}
70-
71-
if (message.type === 'remove') {
72-
return existingMessages;
73-
}
74-
75-
// Find the first message that should be after this one.
76-
let nextMsgIndex = existingMessages.findIndex(
77-
msg => msg.time > message.time
78-
);
79-
if (nextMsgIndex === -1) {
80-
// There is no message after this one, so let's insert the message at
81-
// the end.
82-
nextMsgIndex = existingMessages.length;
83-
}
84-
85-
// Insert the message.
86-
existingMessages.splice(nextMsgIndex, 0, message);
87-
88-
return existingMessages;
89-
});
90-
}
53+
function handleChatEvents(_: IChatModel) {
54+
setMessages([...model.messages]);
9155
}
9256

93-
model.incomingMessage.connect(handleChatEvents);
57+
model.messagesUpdated.connect(handleChatEvents);
9458
return function cleanup() {
95-
model.incomingMessage.disconnect(handleChatEvents);
59+
model.messagesUpdated.disconnect(handleChatEvents);
9660
};
9761
}, [model]);
9862

packages/jupyter-chat/src/model.ts

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
INewMessage,
1212
IChatMessage,
1313
IConfig,
14-
IMessage,
1514
IUser
1615
} from './types';
1716

@@ -35,9 +34,14 @@ export interface IChatModel extends IDisposable {
3534
readonly user?: IUser;
3635

3736
/**
38-
* The signal emitted when a new message is received.
37+
* The chat messages list.
3938
*/
40-
get incomingMessage(): ISignal<IChatModel, IMessage>;
39+
readonly messages: IChatMessage[];
40+
41+
/**
42+
* The signal emitted when the messages list is updated.
43+
*/
44+
readonly messagesUpdated: ISignal<IChatModel, void>;
4145

4246
/**
4347
* Send a message, to be defined depending on the chosen technology.
@@ -49,7 +53,7 @@ export interface IChatModel extends IDisposable {
4953
addMessage(message: INewMessage): Promise<boolean | void> | boolean | void;
5054

5155
/**
52-
* Optional, to update a message from the chat.
56+
* Optional, to update a message from the chat panel.
5357
*
5458
* @param id - the unique ID of the message.
5559
* @param message - the updated message.
@@ -84,9 +88,25 @@ export interface IChatModel extends IDisposable {
8488
/**
8589
* Function to call when a message is received.
8690
*
87-
* @param message - the new message, containing user information and body.
91+
* @param message - the message with user information and body.
92+
*/
93+
messageAdded(message: IChatMessage): void;
94+
95+
/**
96+
* Function called when messages are inserted.
97+
*
98+
* @param index - the index of the first message of the list.
99+
* @param messages - the messages list.
88100
*/
89-
onMessage(message: IMessage): void;
101+
messagesInserted(index: number, messages: IChatMessage[]): void;
102+
103+
/**
104+
* Function called when messages are deleted.
105+
*
106+
* @param index - the index of the first message to delete.
107+
* @param count - the number of messages to delete.
108+
*/
109+
messagesDeleted(index: number, count: number): void;
90110
}
91111

92112
/**
@@ -102,6 +122,13 @@ export class ChatModel implements IChatModel {
102122
this._config = options.config ?? {};
103123
}
104124

125+
/**
126+
* The chat messages list.
127+
*/
128+
get messages(): IChatMessage[] {
129+
return this._messages;
130+
}
131+
105132
/**
106133
* The chat model ID.
107134
*/
@@ -123,11 +150,10 @@ export class ChatModel implements IChatModel {
123150
}
124151

125152
/**
126-
*
127-
* The signal emitted when a new message is received.
153+
* The signal emitted when the messages list is updated.
128154
*/
129-
get incomingMessage(): ISignal<IChatModel, IMessage> {
130-
return this._incomingMessage;
155+
get messagesUpdated(): ISignal<IChatModel, void> {
156+
return this._messagesUpdated;
131157
}
132158

133159
/**
@@ -140,7 +166,7 @@ export class ChatModel implements IChatModel {
140166
addMessage(message: INewMessage): Promise<boolean | void> | boolean | void {}
141167

142168
/**
143-
* Optional, to update a message from the chat.
169+
* Optional, to update a message from the chat panel.
144170
*
145171
* @param id - the unique ID of the message.
146172
* @param message - the message to update.
@@ -180,18 +206,54 @@ export class ChatModel implements IChatModel {
180206
*
181207
* @param message - the message with user information and body.
182208
*/
183-
onMessage(message: IMessage): void {
184-
if (message.type === 'msg') {
185-
message = this.formatChatMessage(message as IChatMessage);
209+
messageAdded(message: IChatMessage): void {
210+
const messageIndex = this._messages.findIndex(msg => msg.id === message.id);
211+
if (messageIndex > -1) {
212+
// The message is an update of an existing one.
213+
// Let's remove it to avoid position conflict if timestamp has changed.
214+
this._messages.splice(messageIndex, 1);
215+
}
216+
// Find the first message that should be after this one.
217+
let nextMsgIndex = this._messages.findIndex(msg => msg.time > message.time);
218+
if (nextMsgIndex === -1) {
219+
// There is no message after this one, so let's insert the message at the end.
220+
nextMsgIndex = this._messages.length;
186221
}
222+
// Insert the message.
223+
this.messagesInserted(nextMsgIndex, [message]);
224+
}
187225

188-
this._incomingMessage.emit(message);
226+
/**
227+
* Function called when messages are inserted.
228+
*
229+
* @param index - the index of the first message of the list.
230+
* @param messages - the messages list.
231+
*/
232+
messagesInserted(index: number, messages: IChatMessage[]): void {
233+
const formattedMessages: IChatMessage[] = [];
234+
messages.forEach(message => {
235+
formattedMessages.push(this.formatChatMessage(message));
236+
});
237+
this._messages.splice(index, 0, ...formattedMessages);
238+
this._messagesUpdated.emit();
239+
}
240+
241+
/**
242+
* Function called when messages are deleted.
243+
*
244+
* @param index - the index of the first message to delete.
245+
* @param count - the number of messages to delete.
246+
*/
247+
messagesDeleted(index: number, count: number): void {
248+
this._messages.splice(index, count);
249+
this._messagesUpdated.emit();
189250
}
190251

252+
private _messages: IChatMessage[] = [];
191253
private _id: string = '';
192254
private _config: IConfig;
193255
private _isDisposed = false;
194-
private _incomingMessage = new Signal<IChatModel, IMessage>(this);
256+
private _messagesUpdated = new Signal<IChatModel, void>(this);
195257
}
196258

197259
/**

packages/jupyter-chat/src/types.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,6 @@ export interface IChatMessage {
3737
edited?: boolean;
3838
}
3939

40-
export type IClearMessage = {
41-
type: 'clear';
42-
};
43-
44-
export type IDeleteMessage = {
45-
type: 'remove';
46-
id: string;
47-
};
48-
49-
export type IMessage = IChatMessage | IClearMessage | IDeleteMessage;
50-
5140
/**
5241
* The chat history interface.
5342
*/

0 commit comments

Comments
 (0)