jupyterlab-chat 0.24.1 → 0.24.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/__tests__/model.spec.js +112 -15
- package/lib/model.d.ts +5 -2
- package/lib/model.js +30 -14
- package/package.json +2 -2
- package/src/__tests__/model.spec.ts +127 -15
- package/src/model.ts +34 -17
|
@@ -14,6 +14,14 @@ const TEST_USER = {
|
|
|
14
14
|
initials: 'TU',
|
|
15
15
|
color: '#aabbcc'
|
|
16
16
|
};
|
|
17
|
+
const TEST_BOT = {
|
|
18
|
+
username: 'test-bot',
|
|
19
|
+
name: 'Test Bot',
|
|
20
|
+
display_name: 'Bot',
|
|
21
|
+
initials: 'BU',
|
|
22
|
+
color: '#aabbcc',
|
|
23
|
+
bot: true
|
|
24
|
+
};
|
|
17
25
|
function makeWidgetConfig() {
|
|
18
26
|
const owner = {};
|
|
19
27
|
owner.config = {};
|
|
@@ -30,13 +38,48 @@ describe('LabChatModel', () => {
|
|
|
30
38
|
user: TEST_USER,
|
|
31
39
|
sharedModel
|
|
32
40
|
});
|
|
33
|
-
|
|
34
|
-
model.id = UUID.uuid4();
|
|
41
|
+
model.markDocumentSynced();
|
|
35
42
|
});
|
|
36
43
|
afterEach(() => {
|
|
37
44
|
Signal.clearData(model);
|
|
38
45
|
sharedModel.dispose();
|
|
39
46
|
});
|
|
47
|
+
describe('message update from model', () => {
|
|
48
|
+
it('should set edited when user message is updated', async () => {
|
|
49
|
+
const id = model.sendMessage({
|
|
50
|
+
body: 'original',
|
|
51
|
+
sender: TEST_USER
|
|
52
|
+
});
|
|
53
|
+
await flushPromises();
|
|
54
|
+
model.updateMessage(id, {
|
|
55
|
+
type: 'msg',
|
|
56
|
+
id,
|
|
57
|
+
body: 'updated',
|
|
58
|
+
time: 1000,
|
|
59
|
+
sender: TEST_USER
|
|
60
|
+
});
|
|
61
|
+
await flushPromises();
|
|
62
|
+
expect(model.messages[0].body).toBe('updated');
|
|
63
|
+
expect(model.messages[0].edited).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
it('should not set edited when bot message is updated', async () => {
|
|
66
|
+
const id = model.sendMessage({
|
|
67
|
+
body: 'original',
|
|
68
|
+
sender: TEST_BOT
|
|
69
|
+
});
|
|
70
|
+
await flushPromises();
|
|
71
|
+
model.updateMessage(id, {
|
|
72
|
+
type: 'msg',
|
|
73
|
+
id,
|
|
74
|
+
body: 'updated',
|
|
75
|
+
time: 1000,
|
|
76
|
+
sender: TEST_BOT
|
|
77
|
+
});
|
|
78
|
+
await flushPromises();
|
|
79
|
+
expect(model.messages[0].body).toBe('updated');
|
|
80
|
+
expect(model.messages[0].edited).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
40
83
|
describe('message attribute updates from shared model', () => {
|
|
41
84
|
it('should reflect a body change', async () => {
|
|
42
85
|
const id = UUID.uuid4();
|
|
@@ -80,26 +123,80 @@ describe('LabChatModel', () => {
|
|
|
80
123
|
await flushPromises();
|
|
81
124
|
expect(model.messages[0].mime_model).toEqual(mimeModel);
|
|
82
125
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
126
|
+
});
|
|
127
|
+
describe('ready resolves after messages are loaded', () => {
|
|
128
|
+
it('messages present in sharedModel before sync are buffered, not yet in model', () => {
|
|
129
|
+
const sm = YChat.create();
|
|
130
|
+
const m = new LabChatModel({
|
|
131
|
+
widgetConfig: makeWidgetConfig(),
|
|
132
|
+
user: TEST_USER,
|
|
133
|
+
sharedModel: sm
|
|
134
|
+
});
|
|
135
|
+
sm.addMessage({
|
|
86
136
|
type: 'msg',
|
|
87
|
-
id,
|
|
88
|
-
body: '',
|
|
137
|
+
id: UUID.uuid4(),
|
|
138
|
+
body: 'pre-sync',
|
|
89
139
|
time: 1000,
|
|
90
140
|
sender: TEST_USER.username
|
|
91
141
|
});
|
|
92
|
-
|
|
93
|
-
|
|
142
|
+
// Not yet synced: message must be buffered, not inserted.
|
|
143
|
+
expect(m.messages).toHaveLength(0);
|
|
144
|
+
sm.dispose();
|
|
145
|
+
});
|
|
146
|
+
it('messages are in model.messages when ready resolves', async () => {
|
|
147
|
+
const sm = YChat.create();
|
|
148
|
+
const m = new LabChatModel({
|
|
149
|
+
widgetConfig: makeWidgetConfig(),
|
|
150
|
+
user: TEST_USER,
|
|
151
|
+
sharedModel: sm
|
|
152
|
+
});
|
|
153
|
+
sm.addMessage({
|
|
94
154
|
type: 'msg',
|
|
95
|
-
id,
|
|
96
|
-
body: '',
|
|
155
|
+
id: UUID.uuid4(),
|
|
156
|
+
body: 'pre-sync',
|
|
97
157
|
time: 1000,
|
|
98
|
-
sender: TEST_USER.username
|
|
99
|
-
mime_model: { data: { 'text/plain': 'streamed output' } }
|
|
158
|
+
sender: TEST_USER.username
|
|
100
159
|
});
|
|
101
|
-
|
|
102
|
-
|
|
160
|
+
m.markDocumentSynced();
|
|
161
|
+
await m.ready;
|
|
162
|
+
expect(m.messages).toHaveLength(1);
|
|
163
|
+
expect(m.messages[0].body).toBe('pre-sync');
|
|
164
|
+
sm.dispose();
|
|
165
|
+
});
|
|
166
|
+
it('model.messages is already populated inside the ready .then() callback', async () => {
|
|
167
|
+
const sm = YChat.create();
|
|
168
|
+
const m = new LabChatModel({
|
|
169
|
+
widgetConfig: makeWidgetConfig(),
|
|
170
|
+
user: TEST_USER,
|
|
171
|
+
sharedModel: sm
|
|
172
|
+
});
|
|
173
|
+
sm.addMessage({
|
|
174
|
+
type: 'msg',
|
|
175
|
+
id: UUID.uuid4(),
|
|
176
|
+
body: 'pre-sync',
|
|
177
|
+
time: 1000,
|
|
178
|
+
sender: TEST_USER.username
|
|
179
|
+
});
|
|
180
|
+
let messagesAtReady = -1;
|
|
181
|
+
const check = m.ready.then(() => {
|
|
182
|
+
messagesAtReady = m.messages.length;
|
|
183
|
+
});
|
|
184
|
+
m.markDocumentSynced();
|
|
185
|
+
await check;
|
|
186
|
+
expect(messagesAtReady).toBe(1);
|
|
187
|
+
sm.dispose();
|
|
188
|
+
});
|
|
189
|
+
it('messages added after sync are inserted directly without buffering', async () => {
|
|
190
|
+
await model.ready;
|
|
191
|
+
sharedModel.addMessage({
|
|
192
|
+
type: 'msg',
|
|
193
|
+
id: UUID.uuid4(),
|
|
194
|
+
body: 'post-sync',
|
|
195
|
+
time: 1000,
|
|
196
|
+
sender: TEST_USER.username
|
|
197
|
+
});
|
|
198
|
+
expect(model.messages).toHaveLength(1);
|
|
199
|
+
expect(model.messages[0].body).toBe('post-sync');
|
|
103
200
|
});
|
|
104
201
|
});
|
|
105
202
|
describe('rerender tracking', () => {
|
package/lib/model.d.ts
CHANGED
|
@@ -63,7 +63,7 @@ export declare class LabChatModel extends AbstractChatModel implements DocumentR
|
|
|
63
63
|
set dirty(value: boolean);
|
|
64
64
|
get readOnly(): boolean;
|
|
65
65
|
set readOnly(value: boolean);
|
|
66
|
-
|
|
66
|
+
protected setReady(): void;
|
|
67
67
|
/**
|
|
68
68
|
* Notify the model that its shared document has been synchronized with the
|
|
69
69
|
* server, and give it an ID if the document does not carry one yet.
|
|
@@ -85,7 +85,8 @@ export declare class LabChatModel extends AbstractChatModel implements DocumentR
|
|
|
85
85
|
toJSON(): PartialJSONObject;
|
|
86
86
|
fromJSON(data: PartialJSONObject): void;
|
|
87
87
|
createChatContext(): IChatContext;
|
|
88
|
-
messagesInserted(index: number, messages: IMessageContent[]):
|
|
88
|
+
messagesInserted(index: number, messages: IMessageContent[]): void;
|
|
89
|
+
private _flushPreReadyMessages;
|
|
89
90
|
sendMessage(message: INewMessage): string | null;
|
|
90
91
|
/**
|
|
91
92
|
* Override the clear messages method.
|
|
@@ -116,6 +117,8 @@ export declare class LabChatModel extends AbstractChatModel implements DocumentR
|
|
|
116
117
|
readonly defaultKernelName: string;
|
|
117
118
|
readonly defaultKernelLanguage: string;
|
|
118
119
|
private _sharedModel;
|
|
120
|
+
private _documentSynced;
|
|
121
|
+
private _preReadyMessages;
|
|
119
122
|
private _dirty;
|
|
120
123
|
private _readOnly;
|
|
121
124
|
private _contentChanged;
|
package/lib/model.js
CHANGED
|
@@ -234,11 +234,14 @@ export class LabChatModel extends AbstractChatModel {
|
|
|
234
234
|
if (changes.stateChange && !this._sharedModel.id) {
|
|
235
235
|
if (changes.stateChange.some(change => change.name === 'dirty' && !change.newValue)) {
|
|
236
236
|
this._sharedModel.id = UUID.uuid4();
|
|
237
|
+
this.setReady();
|
|
237
238
|
}
|
|
238
239
|
}
|
|
239
240
|
};
|
|
240
241
|
this.defaultKernelName = '';
|
|
241
242
|
this.defaultKernelLanguage = '';
|
|
243
|
+
this._documentSynced = false;
|
|
244
|
+
this._preReadyMessages = [];
|
|
242
245
|
this._dirty = false;
|
|
243
246
|
this._readOnly = false;
|
|
244
247
|
this._contentChanged = new Signal(this);
|
|
@@ -294,11 +297,10 @@ export class LabChatModel extends AbstractChatModel {
|
|
|
294
297
|
set readOnly(value) {
|
|
295
298
|
this._readOnly = value;
|
|
296
299
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
300
|
+
setReady() {
|
|
301
|
+
this._documentSynced = true;
|
|
302
|
+
this._flushPreReadyMessages();
|
|
303
|
+
super.setReady();
|
|
302
304
|
}
|
|
303
305
|
/**
|
|
304
306
|
* Notify the model that its shared document has been synchronized with the
|
|
@@ -316,10 +318,13 @@ export class LabChatModel extends AbstractChatModel {
|
|
|
316
318
|
*/
|
|
317
319
|
markDocumentSynced() {
|
|
318
320
|
if (!this._sharedModel.id) {
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
+
// Brand-new document: writing the id fires _onchange synchronously, which
|
|
322
|
+
// sets this.id via metadataChanges before setReady() is called.
|
|
321
323
|
this._sharedModel.id = UUID.uuid4();
|
|
322
324
|
}
|
|
325
|
+
// For an existing document, _onchange already fired for the id metadata
|
|
326
|
+
// during the initial Yjs sync and set this.id before this method was called.
|
|
327
|
+
this.setReady();
|
|
323
328
|
}
|
|
324
329
|
dispose() {
|
|
325
330
|
if (this.isDisposed) {
|
|
@@ -346,12 +351,23 @@ export class LabChatModel extends AbstractChatModel {
|
|
|
346
351
|
createChatContext() {
|
|
347
352
|
return new LabChatContext({ model: this });
|
|
348
353
|
}
|
|
349
|
-
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
|
|
354
|
+
messagesInserted(index, messages) {
|
|
355
|
+
// Buffer messages that arrive before the document is synced. They are
|
|
356
|
+
// flushed synchronously in markDocumentSynced() so that `ready` resolves
|
|
357
|
+
// only after all initial messages and metadata are already in the model.
|
|
358
|
+
// This ensures that the chat has an ID before inserting the messages, to properly
|
|
359
|
+
// catch the unread messages (the last read message is saved using the chat ID).
|
|
360
|
+
if (!this._documentSynced) {
|
|
361
|
+
this._preReadyMessages.push({ index, messages });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
super.messagesInserted(index, messages);
|
|
365
|
+
}
|
|
366
|
+
_flushPreReadyMessages() {
|
|
367
|
+
for (const { index, messages } of this._preReadyMessages) {
|
|
353
368
|
super.messagesInserted(index, messages);
|
|
354
|
-
}
|
|
369
|
+
}
|
|
370
|
+
this._preReadyMessages = [];
|
|
355
371
|
}
|
|
356
372
|
sendMessage(message) {
|
|
357
373
|
var _a, _b, _c, _d, _e;
|
|
@@ -413,7 +429,7 @@ export class LabChatModel extends AbstractChatModel {
|
|
|
413
429
|
let message = this.sharedModel.getMessage(index);
|
|
414
430
|
if (message) {
|
|
415
431
|
message.body = updatedMessage.body;
|
|
416
|
-
message.edited = true;
|
|
432
|
+
message.edited = updatedMessage.sender.bot ? updatedMessage.edited : true;
|
|
417
433
|
}
|
|
418
434
|
else {
|
|
419
435
|
const sender = updatedMessage.sender.username;
|
|
@@ -423,7 +439,7 @@ export class LabChatModel extends AbstractChatModel {
|
|
|
423
439
|
body: updatedMessage.body,
|
|
424
440
|
time: updatedMessage.time || Date.now() / 1000,
|
|
425
441
|
sender: sender,
|
|
426
|
-
edited: true
|
|
442
|
+
edited: updatedMessage.sender.bot ? false : true
|
|
427
443
|
};
|
|
428
444
|
}
|
|
429
445
|
// Update the mime model.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jupyterlab-chat",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.2",
|
|
4
4
|
"description": "The library to build a chat based on shared document",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"watch:src": "tsc -w --sourceMap"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@jupyter/chat": "^0.24.
|
|
45
|
+
"@jupyter/chat": "^0.24.2",
|
|
46
46
|
"@jupyter/collaborative-drive": "^4.4.0 || ^5.0.0",
|
|
47
47
|
"@jupyter/ydoc": "^3.0.0 || ^4.0.0",
|
|
48
48
|
"@jupyterlab/application": "^4.5.0",
|
|
@@ -20,6 +20,15 @@ const TEST_USER = {
|
|
|
20
20
|
color: '#aabbcc'
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
const TEST_BOT = {
|
|
24
|
+
username: 'test-bot',
|
|
25
|
+
name: 'Test Bot',
|
|
26
|
+
display_name: 'Bot',
|
|
27
|
+
initials: 'BU',
|
|
28
|
+
color: '#aabbcc',
|
|
29
|
+
bot: true
|
|
30
|
+
};
|
|
31
|
+
|
|
23
32
|
function makeWidgetConfig(): IWidgetConfig {
|
|
24
33
|
const owner = {} as IWidgetConfig;
|
|
25
34
|
owner.config = {};
|
|
@@ -38,8 +47,7 @@ describe('LabChatModel', () => {
|
|
|
38
47
|
user: TEST_USER,
|
|
39
48
|
sharedModel
|
|
40
49
|
});
|
|
41
|
-
|
|
42
|
-
model.id = UUID.uuid4();
|
|
50
|
+
model.markDocumentSynced();
|
|
43
51
|
});
|
|
44
52
|
|
|
45
53
|
afterEach(() => {
|
|
@@ -47,6 +55,48 @@ describe('LabChatModel', () => {
|
|
|
47
55
|
sharedModel.dispose();
|
|
48
56
|
});
|
|
49
57
|
|
|
58
|
+
describe('message update from model', () => {
|
|
59
|
+
it('should set edited when user message is updated', async () => {
|
|
60
|
+
const id = model.sendMessage({
|
|
61
|
+
body: 'original',
|
|
62
|
+
sender: TEST_USER
|
|
63
|
+
}) as string;
|
|
64
|
+
await flushPromises();
|
|
65
|
+
|
|
66
|
+
model.updateMessage(id, {
|
|
67
|
+
type: 'msg',
|
|
68
|
+
id,
|
|
69
|
+
body: 'updated',
|
|
70
|
+
time: 1000,
|
|
71
|
+
sender: TEST_USER
|
|
72
|
+
});
|
|
73
|
+
await flushPromises();
|
|
74
|
+
|
|
75
|
+
expect(model.messages[0].body).toBe('updated');
|
|
76
|
+
expect(model.messages[0].edited).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should not set edited when bot message is updated', async () => {
|
|
80
|
+
const id = model.sendMessage({
|
|
81
|
+
body: 'original',
|
|
82
|
+
sender: TEST_BOT
|
|
83
|
+
}) as string;
|
|
84
|
+
await flushPromises();
|
|
85
|
+
|
|
86
|
+
model.updateMessage(id, {
|
|
87
|
+
type: 'msg',
|
|
88
|
+
id,
|
|
89
|
+
body: 'updated',
|
|
90
|
+
time: 1000,
|
|
91
|
+
sender: TEST_BOT
|
|
92
|
+
});
|
|
93
|
+
await flushPromises();
|
|
94
|
+
|
|
95
|
+
expect(model.messages[0].body).toBe('updated');
|
|
96
|
+
expect(model.messages[0].edited).toBeUndefined();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
50
100
|
describe('message attribute updates from shared model', () => {
|
|
51
101
|
it('should reflect a body change', async () => {
|
|
52
102
|
const id = UUID.uuid4();
|
|
@@ -95,29 +145,91 @@ describe('LabChatModel', () => {
|
|
|
95
145
|
|
|
96
146
|
expect(model.messages[0].mime_model).toEqual(mimeModel);
|
|
97
147
|
});
|
|
148
|
+
});
|
|
98
149
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
150
|
+
describe('ready resolves after messages are loaded', () => {
|
|
151
|
+
it('messages present in sharedModel before sync are buffered, not yet in model', () => {
|
|
152
|
+
const sm = YChat.create();
|
|
153
|
+
const m = new LabChatModel({
|
|
154
|
+
widgetConfig: makeWidgetConfig(),
|
|
155
|
+
user: TEST_USER,
|
|
156
|
+
sharedModel: sm
|
|
157
|
+
});
|
|
158
|
+
sm.addMessage({
|
|
102
159
|
type: 'msg',
|
|
103
|
-
id,
|
|
104
|
-
body: '',
|
|
160
|
+
id: UUID.uuid4(),
|
|
161
|
+
body: 'pre-sync',
|
|
105
162
|
time: 1000,
|
|
106
163
|
sender: TEST_USER.username
|
|
107
164
|
});
|
|
108
|
-
|
|
165
|
+
// Not yet synced: message must be buffered, not inserted.
|
|
166
|
+
expect(m.messages).toHaveLength(0);
|
|
167
|
+
sm.dispose();
|
|
168
|
+
});
|
|
109
169
|
|
|
110
|
-
|
|
170
|
+
it('messages are in model.messages when ready resolves', async () => {
|
|
171
|
+
const sm = YChat.create();
|
|
172
|
+
const m = new LabChatModel({
|
|
173
|
+
widgetConfig: makeWidgetConfig(),
|
|
174
|
+
user: TEST_USER,
|
|
175
|
+
sharedModel: sm
|
|
176
|
+
});
|
|
177
|
+
sm.addMessage({
|
|
111
178
|
type: 'msg',
|
|
112
|
-
id,
|
|
113
|
-
body: '',
|
|
179
|
+
id: UUID.uuid4(),
|
|
180
|
+
body: 'pre-sync',
|
|
114
181
|
time: 1000,
|
|
115
|
-
sender: TEST_USER.username
|
|
116
|
-
mime_model: { data: { 'text/plain': 'streamed output' } }
|
|
182
|
+
sender: TEST_USER.username
|
|
117
183
|
});
|
|
118
|
-
await flushPromises();
|
|
119
184
|
|
|
120
|
-
|
|
185
|
+
m.markDocumentSynced();
|
|
186
|
+
await m.ready;
|
|
187
|
+
|
|
188
|
+
expect(m.messages).toHaveLength(1);
|
|
189
|
+
expect(m.messages[0].body).toBe('pre-sync');
|
|
190
|
+
sm.dispose();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('model.messages is already populated inside the ready .then() callback', async () => {
|
|
194
|
+
const sm = YChat.create();
|
|
195
|
+
const m = new LabChatModel({
|
|
196
|
+
widgetConfig: makeWidgetConfig(),
|
|
197
|
+
user: TEST_USER,
|
|
198
|
+
sharedModel: sm
|
|
199
|
+
});
|
|
200
|
+
sm.addMessage({
|
|
201
|
+
type: 'msg',
|
|
202
|
+
id: UUID.uuid4(),
|
|
203
|
+
body: 'pre-sync',
|
|
204
|
+
time: 1000,
|
|
205
|
+
sender: TEST_USER.username
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
let messagesAtReady = -1;
|
|
209
|
+
const check = m.ready.then(() => {
|
|
210
|
+
messagesAtReady = m.messages.length;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
m.markDocumentSynced();
|
|
214
|
+
await check;
|
|
215
|
+
|
|
216
|
+
expect(messagesAtReady).toBe(1);
|
|
217
|
+
sm.dispose();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('messages added after sync are inserted directly without buffering', async () => {
|
|
221
|
+
await model.ready;
|
|
222
|
+
|
|
223
|
+
sharedModel.addMessage({
|
|
224
|
+
type: 'msg',
|
|
225
|
+
id: UUID.uuid4(),
|
|
226
|
+
body: 'post-sync',
|
|
227
|
+
time: 1000,
|
|
228
|
+
sender: TEST_USER.username
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(model.messages).toHaveLength(1);
|
|
232
|
+
expect(model.messages[0].body).toBe('post-sync');
|
|
121
233
|
});
|
|
122
234
|
});
|
|
123
235
|
|
package/src/model.ts
CHANGED
|
@@ -162,11 +162,10 @@ export class LabChatModel
|
|
|
162
162
|
this._readOnly = value;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
165
|
+
protected setReady(): void {
|
|
166
|
+
this._documentSynced = true;
|
|
167
|
+
this._flushPreReadyMessages();
|
|
168
|
+
super.setReady();
|
|
170
169
|
}
|
|
171
170
|
|
|
172
171
|
/**
|
|
@@ -185,10 +184,13 @@ export class LabChatModel
|
|
|
185
184
|
*/
|
|
186
185
|
markDocumentSynced(): void {
|
|
187
186
|
if (!this._sharedModel.id) {
|
|
188
|
-
//
|
|
189
|
-
//
|
|
187
|
+
// Brand-new document: writing the id fires _onchange synchronously, which
|
|
188
|
+
// sets this.id via metadataChanges before setReady() is called.
|
|
190
189
|
this._sharedModel.id = UUID.uuid4();
|
|
191
190
|
}
|
|
191
|
+
// For an existing document, _onchange already fired for the id metadata
|
|
192
|
+
// during the initial Yjs sync and set this.id before this method was called.
|
|
193
|
+
this.setReady();
|
|
192
194
|
}
|
|
193
195
|
|
|
194
196
|
dispose(): void {
|
|
@@ -222,15 +224,24 @@ export class LabChatModel
|
|
|
222
224
|
return new LabChatContext({ model: this });
|
|
223
225
|
}
|
|
224
226
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
//
|
|
230
|
-
// unread messages (the last read message is saved using the chat ID).
|
|
231
|
-
|
|
227
|
+
messagesInserted(index: number, messages: IMessageContent[]): void {
|
|
228
|
+
// Buffer messages that arrive before the document is synced. They are
|
|
229
|
+
// flushed synchronously in markDocumentSynced() so that `ready` resolves
|
|
230
|
+
// only after all initial messages and metadata are already in the model.
|
|
231
|
+
// This ensures that the chat has an ID before inserting the messages, to properly
|
|
232
|
+
// catch the unread messages (the last read message is saved using the chat ID).
|
|
233
|
+
if (!this._documentSynced) {
|
|
234
|
+
this._preReadyMessages.push({ index, messages });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
super.messagesInserted(index, messages);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private _flushPreReadyMessages(): void {
|
|
241
|
+
for (const { index, messages } of this._preReadyMessages) {
|
|
232
242
|
super.messagesInserted(index, messages);
|
|
233
|
-
}
|
|
243
|
+
}
|
|
244
|
+
this._preReadyMessages = [];
|
|
234
245
|
}
|
|
235
246
|
|
|
236
247
|
sendMessage(message: INewMessage): string | null {
|
|
@@ -308,7 +319,7 @@ export class LabChatModel
|
|
|
308
319
|
let message = this.sharedModel.getMessage(index);
|
|
309
320
|
if (message) {
|
|
310
321
|
message.body = updatedMessage.body;
|
|
311
|
-
message.edited = true;
|
|
322
|
+
message.edited = updatedMessage.sender.bot ? updatedMessage.edited : true;
|
|
312
323
|
} else {
|
|
313
324
|
const sender = updatedMessage.sender.username;
|
|
314
325
|
|
|
@@ -318,7 +329,7 @@ export class LabChatModel
|
|
|
318
329
|
body: updatedMessage.body,
|
|
319
330
|
time: updatedMessage.time || Date.now() / 1000,
|
|
320
331
|
sender: sender,
|
|
321
|
-
edited: true
|
|
332
|
+
edited: updatedMessage.sender.bot ? false : true
|
|
322
333
|
};
|
|
323
334
|
}
|
|
324
335
|
|
|
@@ -609,6 +620,7 @@ export class LabChatModel
|
|
|
609
620
|
)
|
|
610
621
|
) {
|
|
611
622
|
this._sharedModel.id = UUID.uuid4();
|
|
623
|
+
this.setReady();
|
|
612
624
|
}
|
|
613
625
|
}
|
|
614
626
|
};
|
|
@@ -618,6 +630,11 @@ export class LabChatModel
|
|
|
618
630
|
|
|
619
631
|
private _sharedModel: YChat;
|
|
620
632
|
|
|
633
|
+
private _documentSynced = false;
|
|
634
|
+
private _preReadyMessages: Array<{
|
|
635
|
+
index: number;
|
|
636
|
+
messages: IMessageContent[];
|
|
637
|
+
}> = [];
|
|
621
638
|
private _dirty = false;
|
|
622
639
|
private _readOnly = false;
|
|
623
640
|
private _contentChanged = new Signal<this, void>(this);
|