@jupyter/chat 0.1.0 → 0.2.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/components/chat-input.d.ts +9 -0
- package/lib/components/chat-input.js +110 -9
- package/lib/components/chat-messages.d.ts +45 -15
- package/lib/components/chat-messages.js +237 -54
- package/lib/components/chat.d.ts +21 -6
- package/lib/components/chat.js +15 -44
- package/lib/components/scroll-container.js +1 -19
- package/lib/icons.d.ts +1 -0
- package/lib/icons.js +5 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/model.d.ts +78 -14
- package/lib/model.js +183 -5
- package/lib/registry.d.ts +78 -0
- package/lib/registry.js +83 -0
- package/lib/types.d.ts +56 -4
- package/lib/widgets/chat-sidebar.d.ts +3 -4
- package/lib/widgets/chat-sidebar.js +2 -2
- package/lib/widgets/chat-widget.d.ts +2 -8
- package/lib/widgets/chat-widget.js +6 -6
- package/package.json +202 -200
- package/src/components/chat-input.tsx +182 -45
- package/src/components/chat-messages.tsx +355 -94
- package/src/components/chat.tsx +42 -68
- package/src/components/scroll-container.tsx +1 -25
- package/src/icons.ts +6 -0
- package/src/index.ts +1 -0
- package/src/model.ts +242 -21
- package/src/registry.ts +129 -0
- package/src/types.ts +58 -4
- package/src/widgets/chat-sidebar.tsx +3 -15
- package/src/widgets/chat-widget.tsx +8 -21
- package/style/chat.css +40 -0
- package/style/icons/read.svg +11 -0
package/src/components/chat.tsx
CHANGED
|
@@ -9,57 +9,20 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
|
|
9
9
|
import SettingsIcon from '@mui/icons-material/Settings';
|
|
10
10
|
import { IconButton } from '@mui/material';
|
|
11
11
|
import { Box } from '@mui/system';
|
|
12
|
-
import React, { useState
|
|
12
|
+
import React, { useState } from 'react';
|
|
13
13
|
|
|
14
14
|
import { JlThemeProvider } from './jl-theme-provider';
|
|
15
15
|
import { ChatMessages } from './chat-messages';
|
|
16
16
|
import { ChatInput } from './chat-input';
|
|
17
|
-
import { ScrollContainer } from './scroll-container';
|
|
18
17
|
import { IChatModel } from '../model';
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
type ChatBodyProps = {
|
|
22
|
-
model: IChatModel;
|
|
23
|
-
rmRegistry: IRenderMimeRegistry;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
function ChatBody({
|
|
27
|
-
model,
|
|
28
|
-
rmRegistry: renderMimeRegistry
|
|
29
|
-
}: ChatBodyProps): JSX.Element {
|
|
30
|
-
const [messages, setMessages] = useState<IChatMessage[]>([]);
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Effect: fetch history and config on initial render
|
|
34
|
-
*/
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
async function fetchHistory() {
|
|
37
|
-
if (!model.getHistory) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
model
|
|
41
|
-
.getHistory()
|
|
42
|
-
.then(history => setMessages(history.messages))
|
|
43
|
-
.catch(e => console.error(e));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
fetchHistory();
|
|
47
|
-
}, [model]);
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Effect: listen to chat messages
|
|
51
|
-
*/
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
function handleChatEvents(_: IChatModel) {
|
|
54
|
-
setMessages([...model.messages]);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
model.messagesUpdated.connect(handleChatEvents);
|
|
58
|
-
return function cleanup() {
|
|
59
|
-
model.messagesUpdated.disconnect(handleChatEvents);
|
|
60
|
-
};
|
|
61
|
-
}, [model]);
|
|
18
|
+
import { IAutocompletionRegistry } from '../registry';
|
|
62
19
|
|
|
20
|
+
export function ChatBody(props: Chat.IChatBodyProps): JSX.Element {
|
|
21
|
+
const {
|
|
22
|
+
model,
|
|
23
|
+
rmRegistry: renderMimeRegistry,
|
|
24
|
+
autocompletionRegistry
|
|
25
|
+
} = props;
|
|
63
26
|
// no need to append to messageGroups imperatively here. all of that is
|
|
64
27
|
// handled by the listeners registered in the effect hooks above.
|
|
65
28
|
const onSend = async (input: string) => {
|
|
@@ -69,13 +32,7 @@ function ChatBody({
|
|
|
69
32
|
|
|
70
33
|
return (
|
|
71
34
|
<>
|
|
72
|
-
<
|
|
73
|
-
<ChatMessages
|
|
74
|
-
messages={messages}
|
|
75
|
-
rmRegistry={renderMimeRegistry}
|
|
76
|
-
model={model}
|
|
77
|
-
/>
|
|
78
|
-
</ScrollContainer>
|
|
35
|
+
<ChatMessages rmRegistry={renderMimeRegistry} model={model} />
|
|
79
36
|
<ChatInput
|
|
80
37
|
onSend={onSend}
|
|
81
38
|
sx={{
|
|
@@ -86,15 +43,14 @@ function ChatBody({
|
|
|
86
43
|
borderTop: '1px solid var(--jp-border-color1)'
|
|
87
44
|
}}
|
|
88
45
|
sendWithShiftEnter={model.config.sendWithShiftEnter ?? false}
|
|
46
|
+
autocompletionRegistry={autocompletionRegistry}
|
|
89
47
|
/>
|
|
90
48
|
</>
|
|
91
49
|
);
|
|
92
50
|
}
|
|
93
51
|
|
|
94
52
|
export function Chat(props: Chat.IOptions): JSX.Element {
|
|
95
|
-
const [view, setView] = useState<Chat.
|
|
96
|
-
props.chatView || Chat.ChatView.Chat
|
|
97
|
-
);
|
|
53
|
+
const [view, setView] = useState<Chat.View>(props.chatView || Chat.View.chat);
|
|
98
54
|
return (
|
|
99
55
|
<JlThemeProvider themeManager={props.themeManager ?? null}>
|
|
100
56
|
<Box
|
|
@@ -111,15 +67,15 @@ export function Chat(props: Chat.IOptions): JSX.Element {
|
|
|
111
67
|
>
|
|
112
68
|
{/* top bar */}
|
|
113
69
|
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
114
|
-
{view !== Chat.
|
|
115
|
-
<IconButton onClick={() => setView(Chat.
|
|
70
|
+
{view !== Chat.View.chat ? (
|
|
71
|
+
<IconButton onClick={() => setView(Chat.View.chat)}>
|
|
116
72
|
<ArrowBackIcon />
|
|
117
73
|
</IconButton>
|
|
118
74
|
) : (
|
|
119
75
|
<Box />
|
|
120
76
|
)}
|
|
121
|
-
{view
|
|
122
|
-
<IconButton onClick={() => setView(Chat.
|
|
77
|
+
{view !== Chat.View.settings && props.settingsPanel ? (
|
|
78
|
+
<IconButton onClick={() => setView(Chat.View.settings)}>
|
|
123
79
|
<SettingsIcon />
|
|
124
80
|
</IconButton>
|
|
125
81
|
) : (
|
|
@@ -127,10 +83,14 @@ export function Chat(props: Chat.IOptions): JSX.Element {
|
|
|
127
83
|
)}
|
|
128
84
|
</Box>
|
|
129
85
|
{/* body */}
|
|
130
|
-
{view === Chat.
|
|
131
|
-
<ChatBody
|
|
86
|
+
{view === Chat.View.chat && (
|
|
87
|
+
<ChatBody
|
|
88
|
+
model={props.model}
|
|
89
|
+
rmRegistry={props.rmRegistry}
|
|
90
|
+
autocompletionRegistry={props.autocompletionRegistry}
|
|
91
|
+
/>
|
|
132
92
|
)}
|
|
133
|
-
{view === Chat.
|
|
93
|
+
{view === Chat.View.settings && props.settingsPanel && (
|
|
134
94
|
<props.settingsPanel />
|
|
135
95
|
)}
|
|
136
96
|
</Box>
|
|
@@ -143,9 +103,9 @@ export function Chat(props: Chat.IOptions): JSX.Element {
|
|
|
143
103
|
*/
|
|
144
104
|
export namespace Chat {
|
|
145
105
|
/**
|
|
146
|
-
* The
|
|
106
|
+
* The props for the chat body component.
|
|
147
107
|
*/
|
|
148
|
-
export interface
|
|
108
|
+
export interface IChatBodyProps {
|
|
149
109
|
/**
|
|
150
110
|
* The chat model.
|
|
151
111
|
*/
|
|
@@ -154,6 +114,20 @@ export namespace Chat {
|
|
|
154
114
|
* The rendermime registry.
|
|
155
115
|
*/
|
|
156
116
|
rmRegistry: IRenderMimeRegistry;
|
|
117
|
+
/**
|
|
118
|
+
* Autocompletion registry.
|
|
119
|
+
*/
|
|
120
|
+
autocompletionRegistry?: IAutocompletionRegistry;
|
|
121
|
+
/**
|
|
122
|
+
* Autocompletion name.
|
|
123
|
+
*/
|
|
124
|
+
autocompletionName?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The options to build the Chat UI.
|
|
129
|
+
*/
|
|
130
|
+
export interface IOptions extends IChatBodyProps {
|
|
157
131
|
/**
|
|
158
132
|
* The theme manager.
|
|
159
133
|
*/
|
|
@@ -161,7 +135,7 @@ export namespace Chat {
|
|
|
161
135
|
/**
|
|
162
136
|
* The view to render.
|
|
163
137
|
*/
|
|
164
|
-
chatView?:
|
|
138
|
+
chatView?: View;
|
|
165
139
|
/**
|
|
166
140
|
* A settings panel that can be used for dedicated settings (e.g. jupyter-ai)
|
|
167
141
|
*/
|
|
@@ -172,8 +146,8 @@ export namespace Chat {
|
|
|
172
146
|
* The view to render.
|
|
173
147
|
* The settings view is available only if the settings panel is provided in options.
|
|
174
148
|
*/
|
|
175
|
-
export enum
|
|
176
|
-
|
|
177
|
-
|
|
149
|
+
export enum View {
|
|
150
|
+
chat,
|
|
151
|
+
settings
|
|
178
152
|
}
|
|
179
153
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, {
|
|
6
|
+
import React, { useMemo } from 'react';
|
|
7
7
|
import { Box, SxProps, Theme } from '@mui/material';
|
|
8
8
|
|
|
9
9
|
type ScrollContainerProps = {
|
|
@@ -32,30 +32,6 @@ export function ScrollContainer(props: ScrollContainerProps): JSX.Element {
|
|
|
32
32
|
[]
|
|
33
33
|
);
|
|
34
34
|
|
|
35
|
-
/**
|
|
36
|
-
* Effect: Scroll the container to the bottom as soon as it is visible.
|
|
37
|
-
*/
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
const el = document.querySelector<HTMLElement>(`#${id}`);
|
|
40
|
-
if (!el) {
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const observer = new IntersectionObserver(
|
|
45
|
-
entries => {
|
|
46
|
-
entries.forEach(entry => {
|
|
47
|
-
if (entry.isIntersecting) {
|
|
48
|
-
el.scroll({ top: 999999999 });
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
},
|
|
52
|
-
{ threshold: 1.0 }
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
observer.observe(el);
|
|
56
|
-
return () => observer.disconnect();
|
|
57
|
-
}, []);
|
|
58
|
-
|
|
59
35
|
return (
|
|
60
36
|
<Box
|
|
61
37
|
id={id}
|
package/src/icons.ts
CHANGED
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
import { LabIcon } from '@jupyterlab/ui-components';
|
|
9
9
|
|
|
10
10
|
import chatSvgStr from '../style/icons/chat.svg';
|
|
11
|
+
import readSvgStr from '../style/icons/read.svg';
|
|
11
12
|
|
|
12
13
|
export const chatIcon = new LabIcon({
|
|
13
14
|
name: 'jupyter-chat::chat',
|
|
14
15
|
svgstr: chatSvgStr
|
|
15
16
|
});
|
|
17
|
+
|
|
18
|
+
export const readIcon = new LabIcon({
|
|
19
|
+
name: 'jupyter-chat::read',
|
|
20
|
+
svgstr: readSvgStr
|
|
21
|
+
});
|
package/src/index.ts
CHANGED
package/src/model.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { CommandRegistry } from '@lumino/commands';
|
|
6
7
|
import { IDisposable } from '@lumino/disposable';
|
|
7
8
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
8
9
|
|
|
@@ -19,15 +20,25 @@ import {
|
|
|
19
20
|
*/
|
|
20
21
|
export interface IChatModel extends IDisposable {
|
|
21
22
|
/**
|
|
22
|
-
* The chat model
|
|
23
|
+
* The chat model name.
|
|
23
24
|
*/
|
|
24
|
-
|
|
25
|
+
name: string;
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* The configuration for the chat panel.
|
|
28
29
|
*/
|
|
29
30
|
config: IConfig;
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* The indexes list of the unread messages.
|
|
34
|
+
*/
|
|
35
|
+
unreadMessages: number[];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The indexes list of the messages currently in the viewport.
|
|
39
|
+
*/
|
|
40
|
+
messagesInViewport?: number[];
|
|
41
|
+
|
|
31
42
|
/**
|
|
32
43
|
* The user connected to the chat panel.
|
|
33
44
|
*/
|
|
@@ -39,10 +50,20 @@ export interface IChatModel extends IDisposable {
|
|
|
39
50
|
readonly messages: IChatMessage[];
|
|
40
51
|
|
|
41
52
|
/**
|
|
42
|
-
*
|
|
53
|
+
* A signal emitting when the messages list is updated.
|
|
43
54
|
*/
|
|
44
55
|
readonly messagesUpdated: ISignal<IChatModel, void>;
|
|
45
56
|
|
|
57
|
+
/**
|
|
58
|
+
* A signal emitting when unread messages change.
|
|
59
|
+
*/
|
|
60
|
+
readonly unreadChanged?: ISignal<IChatModel, number[]>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A signal emitting when the viewport change.
|
|
64
|
+
*/
|
|
65
|
+
readonly viewportChanged?: ISignal<IChatModel, number[]>;
|
|
66
|
+
|
|
46
67
|
/**
|
|
47
68
|
* Send a message, to be defined depending on the chosen technology.
|
|
48
69
|
* Default to no-op.
|
|
@@ -119,7 +140,12 @@ export class ChatModel implements IChatModel {
|
|
|
119
140
|
* Create a new chat model.
|
|
120
141
|
*/
|
|
121
142
|
constructor(options: ChatModel.IOptions = {}) {
|
|
122
|
-
|
|
143
|
+
const config = options.config ?? {};
|
|
144
|
+
|
|
145
|
+
// Stack consecutive messages from the same user by default.
|
|
146
|
+
this._config = { stackMessages: true, ...config };
|
|
147
|
+
|
|
148
|
+
this._commands = options.commands;
|
|
123
149
|
}
|
|
124
150
|
|
|
125
151
|
/**
|
|
@@ -130,15 +156,48 @@ export class ChatModel implements IChatModel {
|
|
|
130
156
|
}
|
|
131
157
|
|
|
132
158
|
/**
|
|
133
|
-
* The chat model
|
|
159
|
+
* The chat model id.
|
|
134
160
|
*/
|
|
135
|
-
get id(): string {
|
|
161
|
+
get id(): string | undefined {
|
|
136
162
|
return this._id;
|
|
137
163
|
}
|
|
138
|
-
set id(value: string) {
|
|
164
|
+
set id(value: string | undefined) {
|
|
139
165
|
this._id = value;
|
|
140
166
|
}
|
|
141
167
|
|
|
168
|
+
/**
|
|
169
|
+
* The chat model name.
|
|
170
|
+
*/
|
|
171
|
+
get name(): string {
|
|
172
|
+
return this._name;
|
|
173
|
+
}
|
|
174
|
+
set name(value: string) {
|
|
175
|
+
this._name = value;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Timestamp of the last read message in local storage.
|
|
180
|
+
*/
|
|
181
|
+
get lastRead(): number {
|
|
182
|
+
if (this._id === undefined) {
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
const storage = JSON.parse(
|
|
186
|
+
localStorage.getItem(`@jupyter/chat:${this._id}`) || '{}'
|
|
187
|
+
);
|
|
188
|
+
return storage.lastRead ?? 0;
|
|
189
|
+
}
|
|
190
|
+
set lastRead(value: number) {
|
|
191
|
+
if (this._id === undefined) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const storage = JSON.parse(
|
|
195
|
+
localStorage.getItem(`@jupyter/chat:${this._id}`) || '{}'
|
|
196
|
+
);
|
|
197
|
+
storage.lastRead = value;
|
|
198
|
+
localStorage.setItem(`@jupyter/chat:${this._id}`, JSON.stringify(storage));
|
|
199
|
+
}
|
|
200
|
+
|
|
142
201
|
/**
|
|
143
202
|
* The chat settings.
|
|
144
203
|
*/
|
|
@@ -146,16 +205,103 @@ export class ChatModel implements IChatModel {
|
|
|
146
205
|
return this._config;
|
|
147
206
|
}
|
|
148
207
|
set config(value: Partial<IConfig>) {
|
|
208
|
+
const stackMessagesChanged =
|
|
209
|
+
'stackMessages' in value &&
|
|
210
|
+
this._config.stackMessages !== value.stackMessages;
|
|
211
|
+
|
|
212
|
+
const unreadNotificationsChanged =
|
|
213
|
+
'unreadNotifications' in value &&
|
|
214
|
+
this._config.unreadNotifications !== value.unreadNotifications;
|
|
215
|
+
|
|
149
216
|
this._config = { ...this._config, ...value };
|
|
217
|
+
|
|
218
|
+
if (stackMessagesChanged) {
|
|
219
|
+
if (this._config.stackMessages) {
|
|
220
|
+
this._messages.slice(1).forEach((message, idx) => {
|
|
221
|
+
const previousUser = this._messages[idx].sender.username;
|
|
222
|
+
message.stacked = previousUser === message.sender.username;
|
|
223
|
+
});
|
|
224
|
+
} else {
|
|
225
|
+
this._messages.forEach(message => {
|
|
226
|
+
delete message.stacked;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
this._messagesUpdated.emit();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// remove existing notifications if they are not required anymore.
|
|
233
|
+
if (unreadNotificationsChanged && !this._config.unreadNotifications) {
|
|
234
|
+
this._notify(0);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The indexes list of the unread messages.
|
|
240
|
+
*/
|
|
241
|
+
get unreadMessages(): number[] {
|
|
242
|
+
return this._unreadMessages;
|
|
243
|
+
}
|
|
244
|
+
set unreadMessages(unread: number[]) {
|
|
245
|
+
const recentlyRead = this._unreadMessages.filter(
|
|
246
|
+
elem => !unread.includes(elem)
|
|
247
|
+
);
|
|
248
|
+
const unreadCountDiff = unread.length - this._unreadMessages.length;
|
|
249
|
+
|
|
250
|
+
this._unreadMessages = unread;
|
|
251
|
+
this._unreadChanged.emit(this._unreadMessages);
|
|
252
|
+
|
|
253
|
+
// Notify the change.
|
|
254
|
+
this._notify(unread.length, unreadCountDiff > 0);
|
|
255
|
+
|
|
256
|
+
// Save the last read to the local storage.
|
|
257
|
+
if (this._id !== undefined && recentlyRead.length) {
|
|
258
|
+
let lastReadChanged = false;
|
|
259
|
+
let lastRead = this.lastRead ?? this.messages[recentlyRead[0]].time;
|
|
260
|
+
recentlyRead.forEach(index => {
|
|
261
|
+
if (this.messages[index].time > lastRead) {
|
|
262
|
+
lastRead = this.messages[index].time;
|
|
263
|
+
lastReadChanged = true;
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (lastReadChanged) {
|
|
268
|
+
this.lastRead = lastRead;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The indexes list of the messages currently in the viewport.
|
|
275
|
+
*/
|
|
276
|
+
get messagesInViewport(): number[] {
|
|
277
|
+
return this._messagesInViewport;
|
|
278
|
+
}
|
|
279
|
+
set messagesInViewport(values: number[]) {
|
|
280
|
+
this._messagesInViewport = values;
|
|
281
|
+
this._viewportChanged.emit(values);
|
|
150
282
|
}
|
|
151
283
|
|
|
152
284
|
/**
|
|
153
|
-
*
|
|
285
|
+
* A signal emitting when the messages list is updated.
|
|
154
286
|
*/
|
|
155
287
|
get messagesUpdated(): ISignal<IChatModel, void> {
|
|
156
288
|
return this._messagesUpdated;
|
|
157
289
|
}
|
|
158
290
|
|
|
291
|
+
/**
|
|
292
|
+
* A signal emitting when unread messages change.
|
|
293
|
+
*/
|
|
294
|
+
get unreadChanged(): ISignal<IChatModel, number[]> {
|
|
295
|
+
return this._unreadChanged;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* A signal emitting when the viewport change.
|
|
300
|
+
*/
|
|
301
|
+
get viewportChanged(): ISignal<IChatModel, number[]> {
|
|
302
|
+
return this._viewportChanged;
|
|
303
|
+
}
|
|
304
|
+
|
|
159
305
|
/**
|
|
160
306
|
* Send a message, to be defined depending on the chosen technology.
|
|
161
307
|
* Default to no-op.
|
|
@@ -165,17 +311,6 @@ export class ChatModel implements IChatModel {
|
|
|
165
311
|
*/
|
|
166
312
|
addMessage(message: INewMessage): Promise<boolean | void> | boolean | void {}
|
|
167
313
|
|
|
168
|
-
/**
|
|
169
|
-
* Optional, to update a message from the chat panel.
|
|
170
|
-
*
|
|
171
|
-
* @param id - the unique ID of the message.
|
|
172
|
-
* @param message - the message to update.
|
|
173
|
-
*/
|
|
174
|
-
updateMessage?(
|
|
175
|
-
id: string,
|
|
176
|
-
message: INewMessage
|
|
177
|
-
): Promise<boolean | void> | boolean | void;
|
|
178
|
-
|
|
179
314
|
/**
|
|
180
315
|
* Dispose the chat model.
|
|
181
316
|
*/
|
|
@@ -231,10 +366,35 @@ export class ChatModel implements IChatModel {
|
|
|
231
366
|
*/
|
|
232
367
|
messagesInserted(index: number, messages: IChatMessage[]): void {
|
|
233
368
|
const formattedMessages: IChatMessage[] = [];
|
|
234
|
-
|
|
369
|
+
const unreadIndexes: number[] = [];
|
|
370
|
+
|
|
371
|
+
const lastRead = this.lastRead ?? 0;
|
|
372
|
+
|
|
373
|
+
// Format the messages.
|
|
374
|
+
messages.forEach((message, idx) => {
|
|
235
375
|
formattedMessages.push(this.formatChatMessage(message));
|
|
376
|
+
if (message.time > lastRead) {
|
|
377
|
+
unreadIndexes.push(index + idx);
|
|
378
|
+
}
|
|
236
379
|
});
|
|
380
|
+
|
|
381
|
+
// Insert the messages.
|
|
237
382
|
this._messages.splice(index, 0, ...formattedMessages);
|
|
383
|
+
|
|
384
|
+
if (this._config.stackMessages) {
|
|
385
|
+
// Check if some messages should be stacked by comparing each message' sender
|
|
386
|
+
// with the previous one.
|
|
387
|
+
const lastIdx = index + formattedMessages.length - 1;
|
|
388
|
+
const start = index === 0 ? 1 : index;
|
|
389
|
+
const end = this._messages.length > lastIdx + 1 ? lastIdx + 1 : lastIdx;
|
|
390
|
+
for (let idx = start; idx <= end; idx++) {
|
|
391
|
+
const message = this._messages[idx];
|
|
392
|
+
const previousUser = this._messages[idx - 1].sender.username;
|
|
393
|
+
message.stacked = previousUser === message.sender.username;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
this._addUnreadMessages(unreadIndexes);
|
|
238
398
|
this._messagesUpdated.emit();
|
|
239
399
|
}
|
|
240
400
|
|
|
@@ -249,11 +409,67 @@ export class ChatModel implements IChatModel {
|
|
|
249
409
|
this._messagesUpdated.emit();
|
|
250
410
|
}
|
|
251
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Add unread messages to the list.
|
|
414
|
+
* @param indexes - list of new indexes.
|
|
415
|
+
*/
|
|
416
|
+
private _addUnreadMessages(indexes: number[]) {
|
|
417
|
+
const unread = new Set(this._unreadMessages);
|
|
418
|
+
indexes.forEach(index => unread.add(index));
|
|
419
|
+
this.unreadMessages = Array.from(unread.values());
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Notifications on unread messages.
|
|
424
|
+
*
|
|
425
|
+
* @param unreadCount - number of unread messages.
|
|
426
|
+
* If the value is 0, existing notification will be deleted.
|
|
427
|
+
* @param canCreate - whether to create a notification if it does not exist.
|
|
428
|
+
* Usually it is used when there are new unread messages, and not when the
|
|
429
|
+
* unread messages count decrease.
|
|
430
|
+
*/
|
|
431
|
+
private _notify(unreadCount: number, canCreate: boolean = false) {
|
|
432
|
+
if (this._commands) {
|
|
433
|
+
if (unreadCount && this._config.unreadNotifications) {
|
|
434
|
+
// Update the notification if exist.
|
|
435
|
+
this._commands
|
|
436
|
+
.execute('apputils:update-notification', {
|
|
437
|
+
id: this._notificationId,
|
|
438
|
+
message: `${unreadCount} incoming message(s) ${this._name ? 'in ' + this._name : ''}`
|
|
439
|
+
})
|
|
440
|
+
.then(success => {
|
|
441
|
+
// Create a new notification only if messages are added.
|
|
442
|
+
if (!success && canCreate) {
|
|
443
|
+
this._commands!.execute('apputils:notify', {
|
|
444
|
+
type: 'info',
|
|
445
|
+
message: `${unreadCount} incoming message(s) in ${this._name}`
|
|
446
|
+
}).then(id => {
|
|
447
|
+
this._notificationId = id;
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
} else if (this._notificationId) {
|
|
452
|
+
// Delete notification if there is no more unread messages.
|
|
453
|
+
this._commands.execute('apputils:dismiss-notification', {
|
|
454
|
+
id: this._notificationId
|
|
455
|
+
});
|
|
456
|
+
this._notificationId = null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
252
461
|
private _messages: IChatMessage[] = [];
|
|
253
|
-
private
|
|
462
|
+
private _unreadMessages: number[] = [];
|
|
463
|
+
private _messagesInViewport: number[] = [];
|
|
464
|
+
private _id: string | undefined;
|
|
465
|
+
private _name: string = '';
|
|
254
466
|
private _config: IConfig;
|
|
255
467
|
private _isDisposed = false;
|
|
468
|
+
private _commands?: CommandRegistry;
|
|
469
|
+
private _notificationId: string | null = null;
|
|
256
470
|
private _messagesUpdated = new Signal<IChatModel, void>(this);
|
|
471
|
+
private _unreadChanged = new Signal<IChatModel, number[]>(this);
|
|
472
|
+
private _viewportChanged = new Signal<IChatModel, number[]>(this);
|
|
257
473
|
}
|
|
258
474
|
|
|
259
475
|
/**
|
|
@@ -268,5 +484,10 @@ export namespace ChatModel {
|
|
|
268
484
|
* Initial config for the chat widget.
|
|
269
485
|
*/
|
|
270
486
|
config?: IConfig;
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Commands registry.
|
|
490
|
+
*/
|
|
491
|
+
commands?: CommandRegistry;
|
|
271
492
|
}
|
|
272
493
|
}
|