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 CHANGED
@@ -18,7 +18,34 @@ export declare namespace LabChatModel {
18
18
  }
19
19
  }
20
20
  /**
21
- * The chat model.
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(): IUser[];
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
- * The chat model.
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 === null || mentionsIds === void 0 ? void 0 : mentionsIds.map(user => this.sharedModel.getUser(user) || {
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
- this._user = options.user || { username: 'user undefined' };
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 regex = new RegExp(user.mention_name);
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 = new Set();
350
- // Get the user list from the shared YChat
351
- Object.values(model.sharedModel.users).forEach(userObject => {
352
- users.add(userObject);
353
- });
354
- // Add the users connected to the chat (even if they never sent a message).
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
- const user = value.user;
357
- if (!user) {
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
- users.add(user);
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(value: IUser): void;
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(value) {
180
+ setUser(user) {
181
181
  this.transact(() => {
182
- this._users.set(value.username, value);
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
- var _a;
212
- // Search if the attachment already exist to update it, otherwise add it.
213
- const id = ((_a = Array.from(this._attachments.entries()).find(([_, att]) => att.type === attachment.type && att.value === attachment.value)) === null || _a === void 0 ? void 0 : _a[0]) || UUID.uuid4();
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.14.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.14.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
- * The chat model.
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
- this._user = options.user || { username: 'user undefined' };
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 regex = new RegExp(user.mention_name);
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?.map(
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(): IUser[] {
512
+ get users(): LabChatUser[] {
458
513
  const model = this._model as LabChatModel;
459
- const users = new Set<IUser>();
514
+ const users: Record<string, LabChatUser> = {};
460
515
 
461
- // Get the user list from the shared YChat
462
- Object.values(model.sharedModel.users).forEach(userObject => {
463
- users.add(userObject);
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 the users connected to the chat (even if they never sent a message).
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
- const user = value.user as IUser;
469
- if (!user) {
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
- users.add(user);
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(value: IUser): void {
159
+ setUser(user: IUser): void {
160
160
  this.transact(() => {
161
- this._users.set(value.username, value);
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
- // Search if the attachment already exist to update it, otherwise add it.
198
- const id =
199
- Array.from(this._attachments.entries()).find(
200
- ([_, att]) =>
201
- att.type === attachment.type && att.value === attachment.value
202
- )?.[0] || UUID.uuid4();
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
  });