@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/lib/components/chat.d.ts
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
import { IThemeManager } from '@jupyterlab/apputils';
|
|
3
3
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
4
4
|
import { IChatModel } from '../model';
|
|
5
|
+
import { IAutocompletionRegistry } from '../registry';
|
|
6
|
+
export declare function ChatBody(props: Chat.IChatBodyProps): JSX.Element;
|
|
5
7
|
export declare function Chat(props: Chat.IOptions): JSX.Element;
|
|
6
8
|
/**
|
|
7
9
|
* The chat UI namespace
|
|
8
10
|
*/
|
|
9
11
|
export declare namespace Chat {
|
|
10
12
|
/**
|
|
11
|
-
* The
|
|
13
|
+
* The props for the chat body component.
|
|
12
14
|
*/
|
|
13
|
-
interface
|
|
15
|
+
interface IChatBodyProps {
|
|
14
16
|
/**
|
|
15
17
|
* The chat model.
|
|
16
18
|
*/
|
|
@@ -19,6 +21,19 @@ export declare namespace Chat {
|
|
|
19
21
|
* The rendermime registry.
|
|
20
22
|
*/
|
|
21
23
|
rmRegistry: IRenderMimeRegistry;
|
|
24
|
+
/**
|
|
25
|
+
* Autocompletion registry.
|
|
26
|
+
*/
|
|
27
|
+
autocompletionRegistry?: IAutocompletionRegistry;
|
|
28
|
+
/**
|
|
29
|
+
* Autocompletion name.
|
|
30
|
+
*/
|
|
31
|
+
autocompletionName?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The options to build the Chat UI.
|
|
35
|
+
*/
|
|
36
|
+
interface IOptions extends IChatBodyProps {
|
|
22
37
|
/**
|
|
23
38
|
* The theme manager.
|
|
24
39
|
*/
|
|
@@ -26,7 +41,7 @@ export declare namespace Chat {
|
|
|
26
41
|
/**
|
|
27
42
|
* The view to render.
|
|
28
43
|
*/
|
|
29
|
-
chatView?:
|
|
44
|
+
chatView?: View;
|
|
30
45
|
/**
|
|
31
46
|
* A settings panel that can be used for dedicated settings (e.g. jupyter-ai)
|
|
32
47
|
*/
|
|
@@ -36,8 +51,8 @@ export declare namespace Chat {
|
|
|
36
51
|
* The view to render.
|
|
37
52
|
* The settings view is available only if the settings panel is provided in options.
|
|
38
53
|
*/
|
|
39
|
-
enum
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
enum View {
|
|
55
|
+
chat = 0,
|
|
56
|
+
settings = 1
|
|
42
57
|
}
|
|
43
58
|
}
|
package/lib/components/chat.js
CHANGED
|
@@ -6,41 +6,13 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
|
|
6
6
|
import SettingsIcon from '@mui/icons-material/Settings';
|
|
7
7
|
import { IconButton } from '@mui/material';
|
|
8
8
|
import { Box } from '@mui/system';
|
|
9
|
-
import React, { useState
|
|
9
|
+
import React, { useState } from 'react';
|
|
10
10
|
import { JlThemeProvider } from './jl-theme-provider';
|
|
11
11
|
import { ChatMessages } from './chat-messages';
|
|
12
12
|
import { ChatInput } from './chat-input';
|
|
13
|
-
|
|
14
|
-
function ChatBody({ model, rmRegistry: renderMimeRegistry }) {
|
|
13
|
+
export function ChatBody(props) {
|
|
15
14
|
var _a;
|
|
16
|
-
const
|
|
17
|
-
/**
|
|
18
|
-
* Effect: fetch history and config on initial render
|
|
19
|
-
*/
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
async function fetchHistory() {
|
|
22
|
-
if (!model.getHistory) {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
model
|
|
26
|
-
.getHistory()
|
|
27
|
-
.then(history => setMessages(history.messages))
|
|
28
|
-
.catch(e => console.error(e));
|
|
29
|
-
}
|
|
30
|
-
fetchHistory();
|
|
31
|
-
}, [model]);
|
|
32
|
-
/**
|
|
33
|
-
* Effect: listen to chat messages
|
|
34
|
-
*/
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
function handleChatEvents(_) {
|
|
37
|
-
setMessages([...model.messages]);
|
|
38
|
-
}
|
|
39
|
-
model.messagesUpdated.connect(handleChatEvents);
|
|
40
|
-
return function cleanup() {
|
|
41
|
-
model.messagesUpdated.disconnect(handleChatEvents);
|
|
42
|
-
};
|
|
43
|
-
}, [model]);
|
|
15
|
+
const { model, rmRegistry: renderMimeRegistry, autocompletionRegistry } = props;
|
|
44
16
|
// no need to append to messageGroups imperatively here. all of that is
|
|
45
17
|
// handled by the listeners registered in the effect hooks above.
|
|
46
18
|
const onSend = async (input) => {
|
|
@@ -48,19 +20,18 @@ function ChatBody({ model, rmRegistry: renderMimeRegistry }) {
|
|
|
48
20
|
model.addMessage({ body: input });
|
|
49
21
|
};
|
|
50
22
|
return (React.createElement(React.Fragment, null,
|
|
51
|
-
React.createElement(
|
|
52
|
-
React.createElement(ChatMessages, { messages: messages, rmRegistry: renderMimeRegistry, model: model })),
|
|
23
|
+
React.createElement(ChatMessages, { rmRegistry: renderMimeRegistry, model: model }),
|
|
53
24
|
React.createElement(ChatInput, { onSend: onSend, sx: {
|
|
54
25
|
paddingLeft: 4,
|
|
55
26
|
paddingRight: 4,
|
|
56
27
|
paddingTop: 3.5,
|
|
57
28
|
paddingBottom: 0,
|
|
58
29
|
borderTop: '1px solid var(--jp-border-color1)'
|
|
59
|
-
}, sendWithShiftEnter: (_a = model.config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false })));
|
|
30
|
+
}, sendWithShiftEnter: (_a = model.config.sendWithShiftEnter) !== null && _a !== void 0 ? _a : false, autocompletionRegistry: autocompletionRegistry })));
|
|
60
31
|
}
|
|
61
32
|
export function Chat(props) {
|
|
62
33
|
var _a;
|
|
63
|
-
const [view, setView] = useState(props.chatView || Chat.
|
|
34
|
+
const [view, setView] = useState(props.chatView || Chat.View.chat);
|
|
64
35
|
return (React.createElement(JlThemeProvider, { themeManager: (_a = props.themeManager) !== null && _a !== void 0 ? _a : null },
|
|
65
36
|
React.createElement(Box
|
|
66
37
|
// root box should not include padding as it offsets the vertical
|
|
@@ -77,12 +48,12 @@ export function Chat(props) {
|
|
|
77
48
|
flexDirection: 'column'
|
|
78
49
|
} },
|
|
79
50
|
React.createElement(Box, { sx: { display: 'flex', justifyContent: 'space-between' } },
|
|
80
|
-
view !== Chat.
|
|
51
|
+
view !== Chat.View.chat ? (React.createElement(IconButton, { onClick: () => setView(Chat.View.chat) },
|
|
81
52
|
React.createElement(ArrowBackIcon, null))) : (React.createElement(Box, null)),
|
|
82
|
-
view
|
|
53
|
+
view !== Chat.View.settings && props.settingsPanel ? (React.createElement(IconButton, { onClick: () => setView(Chat.View.settings) },
|
|
83
54
|
React.createElement(SettingsIcon, null))) : (React.createElement(Box, null))),
|
|
84
|
-
view === Chat.
|
|
85
|
-
view === Chat.
|
|
55
|
+
view === Chat.View.chat && (React.createElement(ChatBody, { model: props.model, rmRegistry: props.rmRegistry, autocompletionRegistry: props.autocompletionRegistry })),
|
|
56
|
+
view === Chat.View.settings && props.settingsPanel && (React.createElement(props.settingsPanel, null)))));
|
|
86
57
|
}
|
|
87
58
|
/**
|
|
88
59
|
* The chat UI namespace
|
|
@@ -92,9 +63,9 @@ export function Chat(props) {
|
|
|
92
63
|
* The view to render.
|
|
93
64
|
* The settings view is available only if the settings panel is provided in options.
|
|
94
65
|
*/
|
|
95
|
-
let
|
|
96
|
-
(function (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
})(
|
|
66
|
+
let View;
|
|
67
|
+
(function (View) {
|
|
68
|
+
View[View["chat"] = 0] = "chat";
|
|
69
|
+
View[View["settings"] = 1] = "settings";
|
|
70
|
+
})(View = Chat.View || (Chat.View = {}));
|
|
100
71
|
})(Chat || (Chat = {}));
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
-
import React, {
|
|
5
|
+
import React, { useMemo } from 'react';
|
|
6
6
|
import { Box } from '@mui/material';
|
|
7
7
|
/**
|
|
8
8
|
* Component that handles intelligent scrolling.
|
|
@@ -21,24 +21,6 @@ import { Box } from '@mui/material';
|
|
|
21
21
|
*/
|
|
22
22
|
export function ScrollContainer(props) {
|
|
23
23
|
const id = useMemo(() => 'jupyter-chat-scroll-container-' + Date.now().toString(), []);
|
|
24
|
-
/**
|
|
25
|
-
* Effect: Scroll the container to the bottom as soon as it is visible.
|
|
26
|
-
*/
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
const el = document.querySelector(`#${id}`);
|
|
29
|
-
if (!el) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
const observer = new IntersectionObserver(entries => {
|
|
33
|
-
entries.forEach(entry => {
|
|
34
|
-
if (entry.isIntersecting) {
|
|
35
|
-
el.scroll({ top: 999999999 });
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
}, { threshold: 1.0 });
|
|
39
|
-
observer.observe(el);
|
|
40
|
-
return () => observer.disconnect();
|
|
41
|
-
}, []);
|
|
42
24
|
return (React.createElement(Box, { id: id, sx: {
|
|
43
25
|
overflowY: 'scroll',
|
|
44
26
|
'& *': {
|
package/lib/icons.d.ts
CHANGED
package/lib/icons.js
CHANGED
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
// This file is based on iconimports.ts in @jupyterlab/ui-components, but is manually generated.
|
|
6
6
|
import { LabIcon } from '@jupyterlab/ui-components';
|
|
7
7
|
import chatSvgStr from '../style/icons/chat.svg';
|
|
8
|
+
import readSvgStr from '../style/icons/read.svg';
|
|
8
9
|
export const chatIcon = new LabIcon({
|
|
9
10
|
name: 'jupyter-chat::chat',
|
|
10
11
|
svgstr: chatSvgStr
|
|
11
12
|
});
|
|
13
|
+
export const readIcon = new LabIcon({
|
|
14
|
+
name: 'jupyter-chat::read',
|
|
15
|
+
svgstr: readSvgStr
|
|
16
|
+
});
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
package/lib/model.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CommandRegistry } from '@lumino/commands';
|
|
1
2
|
import { IDisposable } from '@lumino/disposable';
|
|
2
3
|
import { ISignal } from '@lumino/signaling';
|
|
3
4
|
import { IChatHistory, INewMessage, IChatMessage, IConfig, IUser } from './types';
|
|
@@ -6,13 +7,21 @@ import { IChatHistory, INewMessage, IChatMessage, IConfig, IUser } from './types
|
|
|
6
7
|
*/
|
|
7
8
|
export interface IChatModel extends IDisposable {
|
|
8
9
|
/**
|
|
9
|
-
* The chat model
|
|
10
|
+
* The chat model name.
|
|
10
11
|
*/
|
|
11
|
-
|
|
12
|
+
name: string;
|
|
12
13
|
/**
|
|
13
14
|
* The configuration for the chat panel.
|
|
14
15
|
*/
|
|
15
16
|
config: IConfig;
|
|
17
|
+
/**
|
|
18
|
+
* The indexes list of the unread messages.
|
|
19
|
+
*/
|
|
20
|
+
unreadMessages: number[];
|
|
21
|
+
/**
|
|
22
|
+
* The indexes list of the messages currently in the viewport.
|
|
23
|
+
*/
|
|
24
|
+
messagesInViewport?: number[];
|
|
16
25
|
/**
|
|
17
26
|
* The user connected to the chat panel.
|
|
18
27
|
*/
|
|
@@ -22,9 +31,17 @@ export interface IChatModel extends IDisposable {
|
|
|
22
31
|
*/
|
|
23
32
|
readonly messages: IChatMessage[];
|
|
24
33
|
/**
|
|
25
|
-
*
|
|
34
|
+
* A signal emitting when the messages list is updated.
|
|
26
35
|
*/
|
|
27
36
|
readonly messagesUpdated: ISignal<IChatModel, void>;
|
|
37
|
+
/**
|
|
38
|
+
* A signal emitting when unread messages change.
|
|
39
|
+
*/
|
|
40
|
+
readonly unreadChanged?: ISignal<IChatModel, number[]>;
|
|
41
|
+
/**
|
|
42
|
+
* A signal emitting when the viewport change.
|
|
43
|
+
*/
|
|
44
|
+
readonly viewportChanged?: ISignal<IChatModel, number[]>;
|
|
28
45
|
/**
|
|
29
46
|
* Send a message, to be defined depending on the chosen technology.
|
|
30
47
|
* Default to no-op.
|
|
@@ -94,19 +111,47 @@ export declare class ChatModel implements IChatModel {
|
|
|
94
111
|
*/
|
|
95
112
|
get messages(): IChatMessage[];
|
|
96
113
|
/**
|
|
97
|
-
* The chat model
|
|
114
|
+
* The chat model id.
|
|
98
115
|
*/
|
|
99
|
-
get id(): string;
|
|
100
|
-
set id(value: string);
|
|
116
|
+
get id(): string | undefined;
|
|
117
|
+
set id(value: string | undefined);
|
|
118
|
+
/**
|
|
119
|
+
* The chat model name.
|
|
120
|
+
*/
|
|
121
|
+
get name(): string;
|
|
122
|
+
set name(value: string);
|
|
123
|
+
/**
|
|
124
|
+
* Timestamp of the last read message in local storage.
|
|
125
|
+
*/
|
|
126
|
+
get lastRead(): number;
|
|
127
|
+
set lastRead(value: number);
|
|
101
128
|
/**
|
|
102
129
|
* The chat settings.
|
|
103
130
|
*/
|
|
104
131
|
get config(): IConfig;
|
|
105
132
|
set config(value: Partial<IConfig>);
|
|
106
133
|
/**
|
|
107
|
-
* The
|
|
134
|
+
* The indexes list of the unread messages.
|
|
135
|
+
*/
|
|
136
|
+
get unreadMessages(): number[];
|
|
137
|
+
set unreadMessages(unread: number[]);
|
|
138
|
+
/**
|
|
139
|
+
* The indexes list of the messages currently in the viewport.
|
|
140
|
+
*/
|
|
141
|
+
get messagesInViewport(): number[];
|
|
142
|
+
set messagesInViewport(values: number[]);
|
|
143
|
+
/**
|
|
144
|
+
* A signal emitting when the messages list is updated.
|
|
108
145
|
*/
|
|
109
146
|
get messagesUpdated(): ISignal<IChatModel, void>;
|
|
147
|
+
/**
|
|
148
|
+
* A signal emitting when unread messages change.
|
|
149
|
+
*/
|
|
150
|
+
get unreadChanged(): ISignal<IChatModel, number[]>;
|
|
151
|
+
/**
|
|
152
|
+
* A signal emitting when the viewport change.
|
|
153
|
+
*/
|
|
154
|
+
get viewportChanged(): ISignal<IChatModel, number[]>;
|
|
110
155
|
/**
|
|
111
156
|
* Send a message, to be defined depending on the chosen technology.
|
|
112
157
|
* Default to no-op.
|
|
@@ -115,13 +160,6 @@ export declare class ChatModel implements IChatModel {
|
|
|
115
160
|
* @returns whether the message has been sent or not.
|
|
116
161
|
*/
|
|
117
162
|
addMessage(message: INewMessage): Promise<boolean | void> | boolean | void;
|
|
118
|
-
/**
|
|
119
|
-
* Optional, to update a message from the chat panel.
|
|
120
|
-
*
|
|
121
|
-
* @param id - the unique ID of the message.
|
|
122
|
-
* @param message - the message to update.
|
|
123
|
-
*/
|
|
124
|
-
updateMessage?(id: string, message: INewMessage): Promise<boolean | void> | boolean | void;
|
|
125
163
|
/**
|
|
126
164
|
* Dispose the chat model.
|
|
127
165
|
*/
|
|
@@ -155,11 +193,33 @@ export declare class ChatModel implements IChatModel {
|
|
|
155
193
|
* @param count - the number of messages to delete.
|
|
156
194
|
*/
|
|
157
195
|
messagesDeleted(index: number, count: number): void;
|
|
196
|
+
/**
|
|
197
|
+
* Add unread messages to the list.
|
|
198
|
+
* @param indexes - list of new indexes.
|
|
199
|
+
*/
|
|
200
|
+
private _addUnreadMessages;
|
|
201
|
+
/**
|
|
202
|
+
* Notifications on unread messages.
|
|
203
|
+
*
|
|
204
|
+
* @param unreadCount - number of unread messages.
|
|
205
|
+
* If the value is 0, existing notification will be deleted.
|
|
206
|
+
* @param canCreate - whether to create a notification if it does not exist.
|
|
207
|
+
* Usually it is used when there are new unread messages, and not when the
|
|
208
|
+
* unread messages count decrease.
|
|
209
|
+
*/
|
|
210
|
+
private _notify;
|
|
158
211
|
private _messages;
|
|
212
|
+
private _unreadMessages;
|
|
213
|
+
private _messagesInViewport;
|
|
159
214
|
private _id;
|
|
215
|
+
private _name;
|
|
160
216
|
private _config;
|
|
161
217
|
private _isDisposed;
|
|
218
|
+
private _commands?;
|
|
219
|
+
private _notificationId;
|
|
162
220
|
private _messagesUpdated;
|
|
221
|
+
private _unreadChanged;
|
|
222
|
+
private _viewportChanged;
|
|
163
223
|
}
|
|
164
224
|
/**
|
|
165
225
|
* The chat model namespace.
|
|
@@ -173,5 +233,9 @@ export declare namespace ChatModel {
|
|
|
173
233
|
* Initial config for the chat widget.
|
|
174
234
|
*/
|
|
175
235
|
config?: IConfig;
|
|
236
|
+
/**
|
|
237
|
+
* Commands registry.
|
|
238
|
+
*/
|
|
239
|
+
commands?: CommandRegistry;
|
|
176
240
|
}
|
|
177
241
|
}
|
package/lib/model.js
CHANGED
|
@@ -15,10 +15,18 @@ export class ChatModel {
|
|
|
15
15
|
constructor(options = {}) {
|
|
16
16
|
var _a;
|
|
17
17
|
this._messages = [];
|
|
18
|
-
this.
|
|
18
|
+
this._unreadMessages = [];
|
|
19
|
+
this._messagesInViewport = [];
|
|
20
|
+
this._name = '';
|
|
19
21
|
this._isDisposed = false;
|
|
22
|
+
this._notificationId = null;
|
|
20
23
|
this._messagesUpdated = new Signal(this);
|
|
21
|
-
this.
|
|
24
|
+
this._unreadChanged = new Signal(this);
|
|
25
|
+
this._viewportChanged = new Signal(this);
|
|
26
|
+
const config = (_a = options.config) !== null && _a !== void 0 ? _a : {};
|
|
27
|
+
// Stack consecutive messages from the same user by default.
|
|
28
|
+
this._config = { stackMessages: true, ...config };
|
|
29
|
+
this._commands = options.commands;
|
|
22
30
|
}
|
|
23
31
|
/**
|
|
24
32
|
* The chat messages list.
|
|
@@ -27,7 +35,7 @@ export class ChatModel {
|
|
|
27
35
|
return this._messages;
|
|
28
36
|
}
|
|
29
37
|
/**
|
|
30
|
-
* The chat model
|
|
38
|
+
* The chat model id.
|
|
31
39
|
*/
|
|
32
40
|
get id() {
|
|
33
41
|
return this._id;
|
|
@@ -35,6 +43,34 @@ export class ChatModel {
|
|
|
35
43
|
set id(value) {
|
|
36
44
|
this._id = value;
|
|
37
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* The chat model name.
|
|
48
|
+
*/
|
|
49
|
+
get name() {
|
|
50
|
+
return this._name;
|
|
51
|
+
}
|
|
52
|
+
set name(value) {
|
|
53
|
+
this._name = value;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Timestamp of the last read message in local storage.
|
|
57
|
+
*/
|
|
58
|
+
get lastRead() {
|
|
59
|
+
var _a;
|
|
60
|
+
if (this._id === undefined) {
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
const storage = JSON.parse(localStorage.getItem(`@jupyter/chat:${this._id}`) || '{}');
|
|
64
|
+
return (_a = storage.lastRead) !== null && _a !== void 0 ? _a : 0;
|
|
65
|
+
}
|
|
66
|
+
set lastRead(value) {
|
|
67
|
+
if (this._id === undefined) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const storage = JSON.parse(localStorage.getItem(`@jupyter/chat:${this._id}`) || '{}');
|
|
71
|
+
storage.lastRead = value;
|
|
72
|
+
localStorage.setItem(`@jupyter/chat:${this._id}`, JSON.stringify(storage));
|
|
73
|
+
}
|
|
38
74
|
/**
|
|
39
75
|
* The chat settings.
|
|
40
76
|
*/
|
|
@@ -42,14 +78,87 @@ export class ChatModel {
|
|
|
42
78
|
return this._config;
|
|
43
79
|
}
|
|
44
80
|
set config(value) {
|
|
81
|
+
const stackMessagesChanged = 'stackMessages' in value &&
|
|
82
|
+
this._config.stackMessages !== value.stackMessages;
|
|
83
|
+
const unreadNotificationsChanged = 'unreadNotifications' in value &&
|
|
84
|
+
this._config.unreadNotifications !== value.unreadNotifications;
|
|
45
85
|
this._config = { ...this._config, ...value };
|
|
86
|
+
if (stackMessagesChanged) {
|
|
87
|
+
if (this._config.stackMessages) {
|
|
88
|
+
this._messages.slice(1).forEach((message, idx) => {
|
|
89
|
+
const previousUser = this._messages[idx].sender.username;
|
|
90
|
+
message.stacked = previousUser === message.sender.username;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
this._messages.forEach(message => {
|
|
95
|
+
delete message.stacked;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
this._messagesUpdated.emit();
|
|
99
|
+
}
|
|
100
|
+
// remove existing notifications if they are not required anymore.
|
|
101
|
+
if (unreadNotificationsChanged && !this._config.unreadNotifications) {
|
|
102
|
+
this._notify(0);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The indexes list of the unread messages.
|
|
107
|
+
*/
|
|
108
|
+
get unreadMessages() {
|
|
109
|
+
return this._unreadMessages;
|
|
110
|
+
}
|
|
111
|
+
set unreadMessages(unread) {
|
|
112
|
+
var _a;
|
|
113
|
+
const recentlyRead = this._unreadMessages.filter(elem => !unread.includes(elem));
|
|
114
|
+
const unreadCountDiff = unread.length - this._unreadMessages.length;
|
|
115
|
+
this._unreadMessages = unread;
|
|
116
|
+
this._unreadChanged.emit(this._unreadMessages);
|
|
117
|
+
// Notify the change.
|
|
118
|
+
this._notify(unread.length, unreadCountDiff > 0);
|
|
119
|
+
// Save the last read to the local storage.
|
|
120
|
+
if (this._id !== undefined && recentlyRead.length) {
|
|
121
|
+
let lastReadChanged = false;
|
|
122
|
+
let lastRead = (_a = this.lastRead) !== null && _a !== void 0 ? _a : this.messages[recentlyRead[0]].time;
|
|
123
|
+
recentlyRead.forEach(index => {
|
|
124
|
+
if (this.messages[index].time > lastRead) {
|
|
125
|
+
lastRead = this.messages[index].time;
|
|
126
|
+
lastReadChanged = true;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
if (lastReadChanged) {
|
|
130
|
+
this.lastRead = lastRead;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
46
133
|
}
|
|
47
134
|
/**
|
|
48
|
-
* The
|
|
135
|
+
* The indexes list of the messages currently in the viewport.
|
|
136
|
+
*/
|
|
137
|
+
get messagesInViewport() {
|
|
138
|
+
return this._messagesInViewport;
|
|
139
|
+
}
|
|
140
|
+
set messagesInViewport(values) {
|
|
141
|
+
this._messagesInViewport = values;
|
|
142
|
+
this._viewportChanged.emit(values);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* A signal emitting when the messages list is updated.
|
|
49
146
|
*/
|
|
50
147
|
get messagesUpdated() {
|
|
51
148
|
return this._messagesUpdated;
|
|
52
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* A signal emitting when unread messages change.
|
|
152
|
+
*/
|
|
153
|
+
get unreadChanged() {
|
|
154
|
+
return this._unreadChanged;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* A signal emitting when the viewport change.
|
|
158
|
+
*/
|
|
159
|
+
get viewportChanged() {
|
|
160
|
+
return this._viewportChanged;
|
|
161
|
+
}
|
|
53
162
|
/**
|
|
54
163
|
* Send a message, to be defined depending on the chosen technology.
|
|
55
164
|
* Default to no-op.
|
|
@@ -108,11 +217,32 @@ export class ChatModel {
|
|
|
108
217
|
* @param messages - the messages list.
|
|
109
218
|
*/
|
|
110
219
|
messagesInserted(index, messages) {
|
|
220
|
+
var _a;
|
|
111
221
|
const formattedMessages = [];
|
|
112
|
-
|
|
222
|
+
const unreadIndexes = [];
|
|
223
|
+
const lastRead = (_a = this.lastRead) !== null && _a !== void 0 ? _a : 0;
|
|
224
|
+
// Format the messages.
|
|
225
|
+
messages.forEach((message, idx) => {
|
|
113
226
|
formattedMessages.push(this.formatChatMessage(message));
|
|
227
|
+
if (message.time > lastRead) {
|
|
228
|
+
unreadIndexes.push(index + idx);
|
|
229
|
+
}
|
|
114
230
|
});
|
|
231
|
+
// Insert the messages.
|
|
115
232
|
this._messages.splice(index, 0, ...formattedMessages);
|
|
233
|
+
if (this._config.stackMessages) {
|
|
234
|
+
// Check if some messages should be stacked by comparing each message' sender
|
|
235
|
+
// with the previous one.
|
|
236
|
+
const lastIdx = index + formattedMessages.length - 1;
|
|
237
|
+
const start = index === 0 ? 1 : index;
|
|
238
|
+
const end = this._messages.length > lastIdx + 1 ? lastIdx + 1 : lastIdx;
|
|
239
|
+
for (let idx = start; idx <= end; idx++) {
|
|
240
|
+
const message = this._messages[idx];
|
|
241
|
+
const previousUser = this._messages[idx - 1].sender.username;
|
|
242
|
+
message.stacked = previousUser === message.sender.username;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
this._addUnreadMessages(unreadIndexes);
|
|
116
246
|
this._messagesUpdated.emit();
|
|
117
247
|
}
|
|
118
248
|
/**
|
|
@@ -125,4 +255,52 @@ export class ChatModel {
|
|
|
125
255
|
this._messages.splice(index, count);
|
|
126
256
|
this._messagesUpdated.emit();
|
|
127
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Add unread messages to the list.
|
|
260
|
+
* @param indexes - list of new indexes.
|
|
261
|
+
*/
|
|
262
|
+
_addUnreadMessages(indexes) {
|
|
263
|
+
const unread = new Set(this._unreadMessages);
|
|
264
|
+
indexes.forEach(index => unread.add(index));
|
|
265
|
+
this.unreadMessages = Array.from(unread.values());
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Notifications on unread messages.
|
|
269
|
+
*
|
|
270
|
+
* @param unreadCount - number of unread messages.
|
|
271
|
+
* If the value is 0, existing notification will be deleted.
|
|
272
|
+
* @param canCreate - whether to create a notification if it does not exist.
|
|
273
|
+
* Usually it is used when there are new unread messages, and not when the
|
|
274
|
+
* unread messages count decrease.
|
|
275
|
+
*/
|
|
276
|
+
_notify(unreadCount, canCreate = false) {
|
|
277
|
+
if (this._commands) {
|
|
278
|
+
if (unreadCount && this._config.unreadNotifications) {
|
|
279
|
+
// Update the notification if exist.
|
|
280
|
+
this._commands
|
|
281
|
+
.execute('apputils:update-notification', {
|
|
282
|
+
id: this._notificationId,
|
|
283
|
+
message: `${unreadCount} incoming message(s) ${this._name ? 'in ' + this._name : ''}`
|
|
284
|
+
})
|
|
285
|
+
.then(success => {
|
|
286
|
+
// Create a new notification only if messages are added.
|
|
287
|
+
if (!success && canCreate) {
|
|
288
|
+
this._commands.execute('apputils:notify', {
|
|
289
|
+
type: 'info',
|
|
290
|
+
message: `${unreadCount} incoming message(s) in ${this._name}`
|
|
291
|
+
}).then(id => {
|
|
292
|
+
this._notificationId = id;
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
else if (this._notificationId) {
|
|
298
|
+
// Delete notification if there is no more unread messages.
|
|
299
|
+
this._commands.execute('apputils:dismiss-notification', {
|
|
300
|
+
id: this._notificationId
|
|
301
|
+
});
|
|
302
|
+
this._notificationId = null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
128
306
|
}
|