jupyterlab-chat 0.14.0 → 0.16.0
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/model.d.ts +31 -2
- package/lib/model.js +66 -16
- package/lib/widget.js +10 -1
- package/lib/ychat.d.ts +10 -1
- package/lib/ychat.js +27 -5
- package/package.json +2 -2
- package/src/model.ts +78 -17
- package/src/widget.tsx +11 -0
- package/src/ychat.ts +27 -8
package/lib/model.d.ts
CHANGED
|
@@ -18,7 +18,34 @@ export declare namespace LabChatModel {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* A data model class that represents a user and implements the `IUser` interface.
|
|
22
|
+
* Currently, this just just ensures that `user.mention_name` is always
|
|
23
|
+
* accessible by defining it as a getter property.
|
|
24
|
+
*
|
|
25
|
+
* The constructor accepts an `identity: User.IIdentity | IUser | null` object.
|
|
26
|
+
* If `identity == null`, this class provides default values for each required
|
|
27
|
+
* field.
|
|
28
|
+
*
|
|
29
|
+
* TODO: should `identity` (from `LabChatModel.IOptions.user`) ever be `null`?
|
|
30
|
+
*
|
|
31
|
+
* TODO: should this be lifted up into `packages/jupyter-chat`?
|
|
32
|
+
*/
|
|
33
|
+
declare class LabChatUser implements IUser {
|
|
34
|
+
constructor(identity: User.IIdentity | IUser | null);
|
|
35
|
+
get mention_name(): string;
|
|
36
|
+
toJSON(): this & {
|
|
37
|
+
mention_name: string;
|
|
38
|
+
};
|
|
39
|
+
username: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
display_name?: string;
|
|
42
|
+
initials?: string;
|
|
43
|
+
color?: string;
|
|
44
|
+
avatar_url?: string;
|
|
45
|
+
bot?: boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The chat document model.
|
|
22
49
|
*/
|
|
23
50
|
export declare class LabChatModel extends AbstractChatModel implements DocumentRegistry.IModel {
|
|
24
51
|
constructor(options: LabChatModel.IOptions);
|
|
@@ -27,6 +54,7 @@ export declare class LabChatModel extends AbstractChatModel implements DocumentR
|
|
|
27
54
|
get sharedModel(): YChat;
|
|
28
55
|
get contentChanged(): ISignal<this, void>;
|
|
29
56
|
get stateChanged(): ISignal<this, IChangedArgs<any, any, string>>;
|
|
57
|
+
get ready(): Promise<void>;
|
|
30
58
|
get dirty(): boolean;
|
|
31
59
|
set dirty(value: boolean);
|
|
32
60
|
get readOnly(): boolean;
|
|
@@ -85,5 +113,6 @@ export declare class LabChatContext extends AbstractChatContext {
|
|
|
85
113
|
/**
|
|
86
114
|
* The list of users who have connected to this chat.
|
|
87
115
|
*/
|
|
88
|
-
get users():
|
|
116
|
+
get users(): LabChatUser[];
|
|
89
117
|
}
|
|
118
|
+
export {};
|
package/lib/model.js
CHANGED
|
@@ -8,7 +8,44 @@ import { Signal } from '@lumino/signaling';
|
|
|
8
8
|
import { YChat } from './ychat';
|
|
9
9
|
const WRITING_DELAY = 1000;
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* A data model class that represents a user and implements the `IUser` interface.
|
|
12
|
+
* Currently, this just just ensures that `user.mention_name` is always
|
|
13
|
+
* accessible by defining it as a getter property.
|
|
14
|
+
*
|
|
15
|
+
* The constructor accepts an `identity: User.IIdentity | IUser | null` object.
|
|
16
|
+
* If `identity == null`, this class provides default values for each required
|
|
17
|
+
* field.
|
|
18
|
+
*
|
|
19
|
+
* TODO: should `identity` (from `LabChatModel.IOptions.user`) ever be `null`?
|
|
20
|
+
*
|
|
21
|
+
* TODO: should this be lifted up into `packages/jupyter-chat`?
|
|
22
|
+
*/
|
|
23
|
+
class LabChatUser {
|
|
24
|
+
constructor(identity) {
|
|
25
|
+
var _a;
|
|
26
|
+
this.username = (_a = identity === null || identity === void 0 ? void 0 : identity.username) !== null && _a !== void 0 ? _a : 'user undefined';
|
|
27
|
+
this.name = identity === null || identity === void 0 ? void 0 : identity.name;
|
|
28
|
+
this.display_name = identity === null || identity === void 0 ? void 0 : identity.display_name;
|
|
29
|
+
this.color = identity === null || identity === void 0 ? void 0 : identity.color;
|
|
30
|
+
this.avatar_url = identity === null || identity === void 0 ? void 0 : identity.avatar_url;
|
|
31
|
+
this.initials = identity === null || identity === void 0 ? void 0 : identity.initials;
|
|
32
|
+
this.bot = !!(identity === null || identity === void 0 ? void 0 : identity.bot) || false;
|
|
33
|
+
}
|
|
34
|
+
get mention_name() {
|
|
35
|
+
let mention_name = this.display_name || this.name || this.username;
|
|
36
|
+
mention_name = mention_name.replace(/ /g, '-');
|
|
37
|
+
return mention_name;
|
|
38
|
+
}
|
|
39
|
+
toJSON() {
|
|
40
|
+
const simpleObject = {
|
|
41
|
+
...this,
|
|
42
|
+
mention_name: this.mention_name
|
|
43
|
+
};
|
|
44
|
+
return simpleObject;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The chat document model.
|
|
12
49
|
*/
|
|
13
50
|
export class LabChatModel extends AbstractChatModel {
|
|
14
51
|
constructor(options) {
|
|
@@ -81,7 +118,8 @@ export class LabChatModel extends AbstractChatModel {
|
|
|
81
118
|
const msg = {
|
|
82
119
|
...baseMessage,
|
|
83
120
|
sender: this.sharedModel.getUser(sender) || {
|
|
84
|
-
username: 'User undefined'
|
|
121
|
+
username: 'User undefined',
|
|
122
|
+
mention_name: 'User-undefined'
|
|
85
123
|
}
|
|
86
124
|
};
|
|
87
125
|
// Add attachments.
|
|
@@ -97,8 +135,9 @@ export class LabChatModel extends AbstractChatModel {
|
|
|
97
135
|
msg.attachments = attachments;
|
|
98
136
|
}
|
|
99
137
|
}
|
|
100
|
-
const mentions = mentionsIds
|
|
101
|
-
username: 'User undefined'
|
|
138
|
+
const mentions = (mentionsIds !== null && mentionsIds !== void 0 ? mentionsIds : []).map(user => this.sharedModel.getUser(user) || {
|
|
139
|
+
username: 'User undefined',
|
|
140
|
+
mention_name: 'User-undefined'
|
|
102
141
|
});
|
|
103
142
|
if (mentions === null || mentions === void 0 ? void 0 : mentions.length) {
|
|
104
143
|
msg.mentions = mentions;
|
|
@@ -140,7 +179,8 @@ export class LabChatModel extends AbstractChatModel {
|
|
|
140
179
|
this._contentChanged = new Signal(this);
|
|
141
180
|
this._stateChanged = new Signal(this);
|
|
142
181
|
this._timeoutWriting = null;
|
|
143
|
-
|
|
182
|
+
// initialize current user
|
|
183
|
+
this._user = new LabChatUser(options.user);
|
|
144
184
|
const { widgetConfig, sharedModel } = options;
|
|
145
185
|
if (sharedModel) {
|
|
146
186
|
this._sharedModel = sharedModel;
|
|
@@ -169,6 +209,9 @@ export class LabChatModel extends AbstractChatModel {
|
|
|
169
209
|
get stateChanged() {
|
|
170
210
|
return this._stateChanged;
|
|
171
211
|
}
|
|
212
|
+
get ready() {
|
|
213
|
+
return this._ready.promise;
|
|
214
|
+
}
|
|
172
215
|
get dirty() {
|
|
173
216
|
return this._dirty;
|
|
174
217
|
}
|
|
@@ -317,7 +360,8 @@ export class LabChatModel extends AbstractChatModel {
|
|
|
317
360
|
if (!user.mention_name) {
|
|
318
361
|
return;
|
|
319
362
|
}
|
|
320
|
-
const
|
|
363
|
+
const mention = '@' + user.mention_name;
|
|
364
|
+
const regex = new RegExp(mention);
|
|
321
365
|
if (!regex.exec(body)) {
|
|
322
366
|
return;
|
|
323
367
|
}
|
|
@@ -346,19 +390,25 @@ export class LabChatContext extends AbstractChatContext {
|
|
|
346
390
|
*/
|
|
347
391
|
get users() {
|
|
348
392
|
const model = this._model;
|
|
349
|
-
const users =
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
393
|
+
const users = {};
|
|
394
|
+
// Add existing users from the YChat
|
|
395
|
+
// This only includes users who have sent a message in the chat.
|
|
396
|
+
for (const user of Object.values(model.sharedModel.users)) {
|
|
397
|
+
users[user.username] = new LabChatUser(user);
|
|
398
|
+
}
|
|
399
|
+
// Add users from awareness to include connected users even if they never
|
|
400
|
+
// sent a message in the chat.
|
|
355
401
|
model.sharedModel.awareness.getStates().forEach(value => {
|
|
356
|
-
|
|
357
|
-
|
|
402
|
+
if (!('user' in value)) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const userObject = value.user;
|
|
406
|
+
if ((userObject === null || userObject === void 0 ? void 0 : userObject.username) in users) {
|
|
358
407
|
return;
|
|
359
408
|
}
|
|
360
|
-
|
|
409
|
+
const user = new LabChatUser(value.user);
|
|
410
|
+
users[user.username] = user;
|
|
361
411
|
});
|
|
362
|
-
return Array.from(users);
|
|
412
|
+
return Array.from(Object.values(users));
|
|
363
413
|
}
|
|
364
414
|
}
|
package/lib/widget.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { ChatWidget, readIcon } from '@jupyter/chat';
|
|
6
6
|
import { PathExt } from '@jupyterlab/coreutils';
|
|
7
7
|
import { DocumentWidget } from '@jupyterlab/docregistry';
|
|
8
|
-
import { addIcon, closeIcon, CommandToolbarButton, HTMLSelect, launchIcon, PanelWithToolbar, ReactWidget, SidePanel, ToolbarButton } from '@jupyterlab/ui-components';
|
|
8
|
+
import { addIcon, closeIcon, CommandToolbarButton, HTMLSelect, launchIcon, PanelWithToolbar, ReactWidget, SidePanel, Spinner, ToolbarButton } from '@jupyterlab/ui-components';
|
|
9
9
|
import { Signal } from '@lumino/signaling';
|
|
10
10
|
import React, { useState } from 'react';
|
|
11
11
|
import { CommandIDs, chatFileType } from './token';
|
|
@@ -252,7 +252,9 @@ class ChatSection extends PanelWithToolbar {
|
|
|
252
252
|
this._markAsRead.enabled = unread.length > 0;
|
|
253
253
|
// this.title.label = `${unread.length ? '* ' : ''}${this._name}`;
|
|
254
254
|
};
|
|
255
|
+
this._spinner = new Spinner();
|
|
255
256
|
this.addWidget(options.widget);
|
|
257
|
+
this.addWidget(this._spinner);
|
|
256
258
|
this.addClass(SECTION_CLASS);
|
|
257
259
|
this._defaultDirectory = options.defaultDirectory;
|
|
258
260
|
this._path = options.path;
|
|
@@ -291,6 +293,13 @@ class ChatSection extends PanelWithToolbar {
|
|
|
291
293
|
(_a = this.model.unreadChanged) === null || _a === void 0 ? void 0 : _a.connect(this._unreadChanged);
|
|
292
294
|
this._markAsRead.enabled = this.model.unreadMessages.length > 0;
|
|
293
295
|
options.widget.node.style.height = '100%';
|
|
296
|
+
/**
|
|
297
|
+
* Remove the spinner when the chat is ready.
|
|
298
|
+
*/
|
|
299
|
+
const model = this.model;
|
|
300
|
+
model.ready.then(() => {
|
|
301
|
+
this._spinner.dispose();
|
|
302
|
+
});
|
|
294
303
|
}
|
|
295
304
|
/**
|
|
296
305
|
* The path of the chat.
|
package/lib/ychat.d.ts
CHANGED
|
@@ -71,13 +71,22 @@ export declare class YChat extends YDocument<IChatChanges> {
|
|
|
71
71
|
getSource(): JSONObject;
|
|
72
72
|
setSource(value: JSONObject): void;
|
|
73
73
|
getUser(username: string | undefined): IUser | undefined;
|
|
74
|
-
setUser(
|
|
74
|
+
setUser(user: IUser): void;
|
|
75
75
|
getMessage(index: number): IYmessage | undefined;
|
|
76
76
|
addMessage(value: IYmessage): void;
|
|
77
77
|
updateMessage(index: number, value: IYmessage): void;
|
|
78
78
|
getMessageIndex(id: string): number;
|
|
79
79
|
deleteMessage(index: number): void;
|
|
80
80
|
getAttachment(id: string): IAttachment | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Adds or modifies an attachment in the chat, and returns the ID of the
|
|
83
|
+
* attachment.
|
|
84
|
+
*
|
|
85
|
+
* NOTE: this method does not add an attachment to any message. It merely adds
|
|
86
|
+
* the attachment data to the chat file and returns an attachment ID. To add
|
|
87
|
+
* an attachment to a new message, consumers should call this method & add the
|
|
88
|
+
* returned ID to `NewMessage.attachments`.
|
|
89
|
+
*/
|
|
81
90
|
setAttachment(attachment: IAttachment): string;
|
|
82
91
|
private _usersObserver;
|
|
83
92
|
private _messagesObserver;
|
package/lib/ychat.js
CHANGED
|
@@ -177,9 +177,13 @@ export class YChat extends YDocument {
|
|
|
177
177
|
}
|
|
178
178
|
return this._users.get(username);
|
|
179
179
|
}
|
|
180
|
-
setUser(
|
|
180
|
+
setUser(user) {
|
|
181
181
|
this.transact(() => {
|
|
182
|
-
|
|
182
|
+
// call `toJSON()` if the method exists on `user`
|
|
183
|
+
if ('toJSON' in user && typeof user.toJSON === 'function') {
|
|
184
|
+
user = user.toJSON();
|
|
185
|
+
}
|
|
186
|
+
this._users.set(user.username, user);
|
|
183
187
|
});
|
|
184
188
|
}
|
|
185
189
|
getMessage(index) {
|
|
@@ -207,10 +211,28 @@ export class YChat extends YDocument {
|
|
|
207
211
|
getAttachment(id) {
|
|
208
212
|
return this._attachments.get(id);
|
|
209
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Adds or modifies an attachment in the chat, and returns the ID of the
|
|
216
|
+
* attachment.
|
|
217
|
+
*
|
|
218
|
+
* NOTE: this method does not add an attachment to any message. It merely adds
|
|
219
|
+
* the attachment data to the chat file and returns an attachment ID. To add
|
|
220
|
+
* an attachment to a new message, consumers should call this method & add the
|
|
221
|
+
* returned ID to `NewMessage.attachments`.
|
|
222
|
+
*/
|
|
210
223
|
setAttachment(attachment) {
|
|
211
|
-
|
|
212
|
-
//
|
|
213
|
-
const
|
|
224
|
+
// Use the existing ID if the attachment already exists, otherwise create a
|
|
225
|
+
// new ID
|
|
226
|
+
const attachmentJson = JSON.stringify(attachment);
|
|
227
|
+
let existingId;
|
|
228
|
+
for (const [id, att] of this._attachments.entries()) {
|
|
229
|
+
if (JSON.stringify(att) === attachmentJson) {
|
|
230
|
+
existingId = id;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const id = existingId || UUID.uuid4();
|
|
235
|
+
// Set the attachment using the computed ID, then return the ID
|
|
214
236
|
this.transact(() => {
|
|
215
237
|
this._attachments.set(id, attachment);
|
|
216
238
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jupyterlab-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
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.
|
|
45
|
+
"@jupyter/chat": "^0.16.0",
|
|
46
46
|
"@jupyter/collaborative-drive": "^4.0.2",
|
|
47
47
|
"@jupyter/ydoc": "^2.0.0 || ^3.0.0",
|
|
48
48
|
"@jupyterlab/application": "^4.2.0",
|
package/src/model.ts
CHANGED
|
@@ -38,7 +38,54 @@ export namespace LabChatModel {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
41
|
+
* A data model class that represents a user and implements the `IUser` interface.
|
|
42
|
+
* Currently, this just just ensures that `user.mention_name` is always
|
|
43
|
+
* accessible by defining it as a getter property.
|
|
44
|
+
*
|
|
45
|
+
* The constructor accepts an `identity: User.IIdentity | IUser | null` object.
|
|
46
|
+
* If `identity == null`, this class provides default values for each required
|
|
47
|
+
* field.
|
|
48
|
+
*
|
|
49
|
+
* TODO: should `identity` (from `LabChatModel.IOptions.user`) ever be `null`?
|
|
50
|
+
*
|
|
51
|
+
* TODO: should this be lifted up into `packages/jupyter-chat`?
|
|
52
|
+
*/
|
|
53
|
+
class LabChatUser implements IUser {
|
|
54
|
+
constructor(identity: User.IIdentity | IUser | null) {
|
|
55
|
+
this.username = identity?.username ?? 'user undefined';
|
|
56
|
+
this.name = identity?.name;
|
|
57
|
+
this.display_name = identity?.display_name;
|
|
58
|
+
this.color = identity?.color;
|
|
59
|
+
this.avatar_url = identity?.avatar_url;
|
|
60
|
+
this.initials = identity?.initials;
|
|
61
|
+
this.bot = !!identity?.bot || false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get mention_name(): string {
|
|
65
|
+
let mention_name = this.display_name || this.name || this.username;
|
|
66
|
+
mention_name = mention_name.replace(/ /g, '-');
|
|
67
|
+
return mention_name;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
toJSON() {
|
|
71
|
+
const simpleObject = {
|
|
72
|
+
...this,
|
|
73
|
+
mention_name: this.mention_name
|
|
74
|
+
};
|
|
75
|
+
return simpleObject;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
username: string;
|
|
79
|
+
name?: string;
|
|
80
|
+
display_name?: string;
|
|
81
|
+
initials?: string;
|
|
82
|
+
color?: string;
|
|
83
|
+
avatar_url?: string;
|
|
84
|
+
bot?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The chat document model.
|
|
42
89
|
*/
|
|
43
90
|
export class LabChatModel
|
|
44
91
|
extends AbstractChatModel
|
|
@@ -47,7 +94,8 @@ export class LabChatModel
|
|
|
47
94
|
constructor(options: LabChatModel.IOptions) {
|
|
48
95
|
super(options);
|
|
49
96
|
|
|
50
|
-
|
|
97
|
+
// initialize current user
|
|
98
|
+
this._user = new LabChatUser(options.user);
|
|
51
99
|
|
|
52
100
|
const { widgetConfig, sharedModel } = options;
|
|
53
101
|
|
|
@@ -89,6 +137,10 @@ export class LabChatModel
|
|
|
89
137
|
return this._stateChanged;
|
|
90
138
|
}
|
|
91
139
|
|
|
140
|
+
get ready(): Promise<void> {
|
|
141
|
+
return this._ready.promise;
|
|
142
|
+
}
|
|
143
|
+
|
|
92
144
|
get dirty(): boolean {
|
|
93
145
|
return this._dirty;
|
|
94
146
|
}
|
|
@@ -331,7 +383,8 @@ export class LabChatModel
|
|
|
331
383
|
if (!user.mention_name) {
|
|
332
384
|
return;
|
|
333
385
|
}
|
|
334
|
-
const
|
|
386
|
+
const mention = '@' + user.mention_name;
|
|
387
|
+
const regex = new RegExp(mention);
|
|
335
388
|
if (!regex.exec(body)) {
|
|
336
389
|
return;
|
|
337
390
|
}
|
|
@@ -373,7 +426,8 @@ export class LabChatModel
|
|
|
373
426
|
const msg: IChatMessage = {
|
|
374
427
|
...baseMessage,
|
|
375
428
|
sender: this.sharedModel.getUser(sender) || {
|
|
376
|
-
username: 'User undefined'
|
|
429
|
+
username: 'User undefined',
|
|
430
|
+
mention_name: 'User-undefined'
|
|
377
431
|
}
|
|
378
432
|
};
|
|
379
433
|
|
|
@@ -391,10 +445,11 @@ export class LabChatModel
|
|
|
391
445
|
}
|
|
392
446
|
}
|
|
393
447
|
|
|
394
|
-
const mentions = mentionsIds
|
|
448
|
+
const mentions: IUser[] = (mentionsIds ?? []).map(
|
|
395
449
|
user =>
|
|
396
450
|
this.sharedModel.getUser(user) || {
|
|
397
|
-
username: 'User undefined'
|
|
451
|
+
username: 'User undefined',
|
|
452
|
+
mention_name: 'User-undefined'
|
|
398
453
|
}
|
|
399
454
|
);
|
|
400
455
|
|
|
@@ -454,24 +509,30 @@ export class LabChatContext extends AbstractChatContext {
|
|
|
454
509
|
/**
|
|
455
510
|
* The list of users who have connected to this chat.
|
|
456
511
|
*/
|
|
457
|
-
get users():
|
|
512
|
+
get users(): LabChatUser[] {
|
|
458
513
|
const model = this._model as LabChatModel;
|
|
459
|
-
const users =
|
|
514
|
+
const users: Record<string, LabChatUser> = {};
|
|
460
515
|
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
516
|
+
// Add existing users from the YChat
|
|
517
|
+
// This only includes users who have sent a message in the chat.
|
|
518
|
+
for (const user of Object.values(model.sharedModel.users)) {
|
|
519
|
+
users[user.username] = new LabChatUser(user);
|
|
520
|
+
}
|
|
465
521
|
|
|
466
|
-
// Add
|
|
522
|
+
// Add users from awareness to include connected users even if they never
|
|
523
|
+
// sent a message in the chat.
|
|
467
524
|
model.sharedModel.awareness.getStates().forEach(value => {
|
|
468
|
-
|
|
469
|
-
|
|
525
|
+
if (!('user' in value)) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const userObject = value.user as IUser;
|
|
529
|
+
if (userObject?.username in users) {
|
|
470
530
|
return;
|
|
471
531
|
}
|
|
472
|
-
|
|
532
|
+
const user = new LabChatUser(value.user as IUser);
|
|
533
|
+
users[user.username] = user;
|
|
473
534
|
});
|
|
474
535
|
|
|
475
|
-
return Array.from(users);
|
|
536
|
+
return Array.from(Object.values(users));
|
|
476
537
|
}
|
|
477
538
|
}
|
package/src/widget.tsx
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
PanelWithToolbar,
|
|
27
27
|
ReactWidget,
|
|
28
28
|
SidePanel,
|
|
29
|
+
Spinner,
|
|
29
30
|
ToolbarButton
|
|
30
31
|
} from '@jupyterlab/ui-components';
|
|
31
32
|
import { CommandRegistry } from '@lumino/commands';
|
|
@@ -343,6 +344,7 @@ class ChatSection extends PanelWithToolbar {
|
|
|
343
344
|
super(options);
|
|
344
345
|
|
|
345
346
|
this.addWidget(options.widget);
|
|
347
|
+
this.addWidget(this._spinner);
|
|
346
348
|
|
|
347
349
|
this.addClass(SECTION_CLASS);
|
|
348
350
|
this._defaultDirectory = options.defaultDirectory;
|
|
@@ -389,6 +391,14 @@ class ChatSection extends PanelWithToolbar {
|
|
|
389
391
|
this._markAsRead.enabled = this.model.unreadMessages.length > 0;
|
|
390
392
|
|
|
391
393
|
options.widget.node.style.height = '100%';
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Remove the spinner when the chat is ready.
|
|
397
|
+
*/
|
|
398
|
+
const model = this.model as LabChatModel;
|
|
399
|
+
model.ready.then(() => {
|
|
400
|
+
this._spinner.dispose();
|
|
401
|
+
});
|
|
392
402
|
}
|
|
393
403
|
|
|
394
404
|
/**
|
|
@@ -458,6 +468,7 @@ class ChatSection extends PanelWithToolbar {
|
|
|
458
468
|
private _defaultDirectory: string;
|
|
459
469
|
private _markAsRead: ToolbarButton;
|
|
460
470
|
private _path: string;
|
|
471
|
+
private _spinner = new Spinner();
|
|
461
472
|
}
|
|
462
473
|
|
|
463
474
|
/**
|
package/src/ychat.ts
CHANGED
|
@@ -156,9 +156,13 @@ export class YChat extends YDocument<IChatChanges> {
|
|
|
156
156
|
return this._users.get(username);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
setUser(
|
|
159
|
+
setUser(user: IUser): void {
|
|
160
160
|
this.transact(() => {
|
|
161
|
-
|
|
161
|
+
// call `toJSON()` if the method exists on `user`
|
|
162
|
+
if ('toJSON' in user && typeof user.toJSON === 'function') {
|
|
163
|
+
user = user.toJSON();
|
|
164
|
+
}
|
|
165
|
+
this._users.set(user.username, user);
|
|
162
166
|
});
|
|
163
167
|
}
|
|
164
168
|
|
|
@@ -193,14 +197,29 @@ export class YChat extends YDocument<IChatChanges> {
|
|
|
193
197
|
return this._attachments.get(id);
|
|
194
198
|
}
|
|
195
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Adds or modifies an attachment in the chat, and returns the ID of the
|
|
202
|
+
* attachment.
|
|
203
|
+
*
|
|
204
|
+
* NOTE: this method does not add an attachment to any message. It merely adds
|
|
205
|
+
* the attachment data to the chat file and returns an attachment ID. To add
|
|
206
|
+
* an attachment to a new message, consumers should call this method & add the
|
|
207
|
+
* returned ID to `NewMessage.attachments`.
|
|
208
|
+
*/
|
|
196
209
|
setAttachment(attachment: IAttachment): string {
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
210
|
+
// Use the existing ID if the attachment already exists, otherwise create a
|
|
211
|
+
// new ID
|
|
212
|
+
const attachmentJson = JSON.stringify(attachment);
|
|
213
|
+
let existingId: string | undefined;
|
|
214
|
+
for (const [id, att] of this._attachments.entries()) {
|
|
215
|
+
if (JSON.stringify(att) === attachmentJson) {
|
|
216
|
+
existingId = id;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const id = existingId || UUID.uuid4();
|
|
203
221
|
|
|
222
|
+
// Set the attachment using the computed ID, then return the ID
|
|
204
223
|
this.transact(() => {
|
|
205
224
|
this._attachments.set(id, attachment);
|
|
206
225
|
});
|