@jupyter/chat 0.22.1 → 0.23.0-alpha.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/__tests__/multichat-panel.spec.js +82 -1
- package/lib/components/attachments.d.ts +0 -1
- package/lib/components/avatar.d.ts +0 -1
- package/lib/components/chat.d.ts +0 -1
- package/lib/components/chat.js +8 -9
- package/lib/components/code-blocks/code-toolbar.d.ts +0 -1
- package/lib/components/code-blocks/copy-button.d.ts +0 -1
- package/lib/components/input/buttons/attach-button.d.ts +0 -1
- package/lib/components/input/buttons/cancel-button.d.ts +0 -1
- package/lib/components/input/buttons/save-edit-button.d.ts +0 -1
- package/lib/components/input/buttons/save-edit-button.js +2 -2
- package/lib/components/input/buttons/send-button.d.ts +0 -1
- package/lib/components/input/buttons/send-button.js +3 -10
- package/lib/components/input/buttons/stop-button.d.ts +0 -1
- package/lib/components/input/chat-input.d.ts +0 -1
- package/lib/components/input/chat-input.js +2 -3
- package/lib/components/input/submit-message.d.ts +22 -0
- package/lib/components/input/submit-message.js +13 -0
- package/lib/components/messages/chat-body-placeholder.d.ts +0 -1
- package/lib/components/messages/footer.d.ts +0 -1
- package/lib/components/messages/header.d.ts +3 -2
- package/lib/components/messages/header.js +2 -1
- package/lib/components/messages/message.d.ts +3 -6
- package/lib/components/messages/message.js +26 -14
- package/lib/components/messages/messages.d.ts +0 -1
- package/lib/components/messages/messages.js +11 -20
- package/lib/components/messages/preamble.d.ts +0 -1
- package/lib/components/messages/toolbar.d.ts +0 -1
- package/lib/components/messages/welcome.d.ts +0 -1
- package/lib/components/mui-extras/tooltipped-icon-button.d.ts +0 -1
- package/lib/components/writing-indicator.d.ts +0 -1
- package/lib/context.d.ts +0 -1
- package/lib/message.d.ts +6 -0
- package/lib/message.js +23 -0
- package/lib/model.d.ts +8 -1
- package/lib/model.js +3 -0
- package/lib/registers/chat-commands.d.ts +0 -1
- package/lib/registers/footers.d.ts +0 -1
- package/lib/registers/preambles.d.ts +0 -1
- package/lib/tokens.d.ts +0 -1
- package/lib/types.d.ts +5 -0
- package/lib/widgets/chat-selector-popup.d.ts +0 -1
- package/lib/widgets/multichat-panel.d.ts +21 -5
- package/lib/widgets/multichat-panel.js +50 -10
- package/package.json +22 -22
- package/src/__tests__/multichat-panel.spec.ts +113 -1
- package/src/components/chat.tsx +10 -10
- package/src/components/input/buttons/save-edit-button.tsx +2 -2
- package/src/components/input/buttons/send-button.tsx +3 -13
- package/src/components/input/chat-input.tsx +2 -3
- package/src/components/input/submit-message.ts +38 -0
- package/src/components/messages/header.tsx +5 -1
- package/src/components/messages/message.tsx +129 -122
- package/src/components/messages/messages.tsx +14 -26
- package/src/message.ts +29 -0
- package/src/model.ts +10 -2
- package/src/types.ts +5 -0
- package/src/widgets/multichat-panel.tsx +62 -11
- package/style/chat.css +8 -4
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IInputModel } from '../../input-model';
|
|
7
|
+
import { IChatCommandRegistry } from '../../registers';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Submit the current input value as a chat message.
|
|
11
|
+
*/
|
|
12
|
+
export async function submitInputMessage(
|
|
13
|
+
options: submitInputMessage.IOptions
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const { model, chatCommandRegistry, body } = options;
|
|
16
|
+
|
|
17
|
+
await chatCommandRegistry?.onSubmit(model);
|
|
18
|
+
|
|
19
|
+
model.send(body ?? model.value);
|
|
20
|
+
model.focus();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export namespace submitInputMessage {
|
|
24
|
+
export interface IOptions {
|
|
25
|
+
/**
|
|
26
|
+
* The input model containing the message to send.
|
|
27
|
+
*/
|
|
28
|
+
model: IInputModel;
|
|
29
|
+
/**
|
|
30
|
+
* Optional chat command registry used to preprocess message content.
|
|
31
|
+
*/
|
|
32
|
+
chatCommandRegistry?: IChatCommandRegistry;
|
|
33
|
+
/**
|
|
34
|
+
* Optional message body to send instead of the current model value.
|
|
35
|
+
*/
|
|
36
|
+
body?: string;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -30,7 +30,9 @@ type ChatMessageHeaderProps = {
|
|
|
30
30
|
/**
|
|
31
31
|
* The message header component.
|
|
32
32
|
*/
|
|
33
|
-
export function
|
|
33
|
+
export function ChatMessageHeaderBase(
|
|
34
|
+
props: ChatMessageHeaderProps
|
|
35
|
+
): JSX.Element {
|
|
34
36
|
const trans = useTranslator();
|
|
35
37
|
const [message, setMessage] = useState<IMessageContent>(
|
|
36
38
|
props.message.content
|
|
@@ -163,3 +165,5 @@ export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
|
|
|
163
165
|
</Box>
|
|
164
166
|
);
|
|
165
167
|
}
|
|
168
|
+
|
|
169
|
+
export const ChatMessageHeader = React.memo(ChatMessageHeaderBase);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { PromiseDelegate } from '@lumino/coreutils';
|
|
7
|
-
import React, {
|
|
7
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
8
8
|
|
|
9
9
|
import { MessageRenderer } from './message-renderer';
|
|
10
10
|
import { AttachmentPreviewList } from '../attachments';
|
|
@@ -14,7 +14,7 @@ import { IInputModel, InputModel } from '../../input-model';
|
|
|
14
14
|
import { IMessageContent, IMessage } from '../../types';
|
|
15
15
|
import { replaceSpanToMention } from '../../utils';
|
|
16
16
|
|
|
17
|
-
const MESSAGE_CONTAINER_CLASS = 'jp-chat-message-container';
|
|
17
|
+
export const MESSAGE_CONTAINER_CLASS = 'jp-chat-message-container';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* The message component props.
|
|
@@ -28,103 +28,100 @@ type ChatMessageProps = {
|
|
|
28
28
|
* The index of the message in the list.
|
|
29
29
|
*/
|
|
30
30
|
index: number;
|
|
31
|
-
/**
|
|
32
|
-
* The promise to resolve when the message is rendered.
|
|
33
|
-
*/
|
|
34
|
-
renderedPromise: PromiseDelegate<void>;
|
|
35
31
|
};
|
|
36
32
|
|
|
37
33
|
/**
|
|
38
34
|
* The message component body.
|
|
39
35
|
*/
|
|
40
|
-
export
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
// Init canDelete and canEdit state.
|
|
54
|
-
setDeleted(message.deleted ?? false);
|
|
55
|
-
if (model.user !== undefined && !message.deleted) {
|
|
56
|
-
if (model.user.username === message.sender.username) {
|
|
57
|
-
setCanEdit(model.updateMessage !== undefined);
|
|
58
|
-
setCanDelete(model.deleteMessage !== undefined);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (message.sender.bot) {
|
|
62
|
-
setCanDelete(model.deleteMessage !== undefined);
|
|
63
|
-
}
|
|
64
|
-
} else {
|
|
65
|
-
setCanEdit(false);
|
|
66
|
-
setCanDelete(false);
|
|
67
|
-
}
|
|
68
|
-
}, [model, message]);
|
|
36
|
+
export function ChatMessageBase(props: ChatMessageProps): JSX.Element {
|
|
37
|
+
const { model } = useChatContext();
|
|
38
|
+
const [message, setMessage] = useState<IMessageContent>(
|
|
39
|
+
props.message.content
|
|
40
|
+
);
|
|
41
|
+
const [renderedDelegate, setRenderedDelegate] = useState<
|
|
42
|
+
PromiseDelegate<void>
|
|
43
|
+
>(props.message.renderedDelegate);
|
|
44
|
+
const [edit, setEdit] = useState<boolean>(false);
|
|
45
|
+
const [deleted, setDeleted] = useState<boolean>(false);
|
|
46
|
+
const [canEdit, setCanEdit] = useState<boolean>(false);
|
|
47
|
+
const [canDelete, setCanDelete] = useState<boolean>(false);
|
|
69
48
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
49
|
+
// Look if the message can be deleted or edited.
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
// Init canDelete and canEdit state.
|
|
52
|
+
setDeleted(message.deleted ?? false);
|
|
53
|
+
if (model.user !== undefined && !message.deleted) {
|
|
54
|
+
if (model.user.username === message.sender.username) {
|
|
55
|
+
setCanEdit(model.updateMessage !== undefined);
|
|
56
|
+
setCanDelete(model.deleteMessage !== undefined);
|
|
57
|
+
return;
|
|
74
58
|
}
|
|
59
|
+
if (message.sender.bot) {
|
|
60
|
+
setCanDelete(model.deleteMessage !== undefined);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
setCanEdit(false);
|
|
64
|
+
setCanDelete(false);
|
|
65
|
+
}
|
|
66
|
+
}, [model, message]);
|
|
75
67
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// FIX ? This seems to be required for outofband change, to get the new value,
|
|
80
|
-
// even if when an outofband change occurs, all the messages are deleted and
|
|
81
|
-
// recreated.
|
|
68
|
+
// Listen for changes in the current message.
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
function messageChanged() {
|
|
82
71
|
setMessage(props.message.content);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
};
|
|
86
|
-
}, [props.message]);
|
|
87
|
-
|
|
88
|
-
// Create an input model only if the message is edited.
|
|
89
|
-
const startEdition = (): void => {
|
|
90
|
-
if (!canEdit || !(typeof message.body === 'string')) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
let body = message.body;
|
|
94
|
-
message.mentions?.forEach(user => {
|
|
95
|
-
body = replaceSpanToMention(body, user);
|
|
96
|
-
});
|
|
97
|
-
const inputModel = new InputModel({
|
|
98
|
-
chatContext: model.createChatContext(),
|
|
99
|
-
onSend: (input: string, model?: IInputModel) =>
|
|
100
|
-
updateMessage(message.id, input, model),
|
|
101
|
-
onCancel: () => cancelEdition(),
|
|
102
|
-
value: body,
|
|
103
|
-
activeCellManager: model.activeCellManager,
|
|
104
|
-
selectionWatcher: model.selectionWatcher,
|
|
105
|
-
documentManager: model.documentManager,
|
|
106
|
-
config: {
|
|
107
|
-
sendWithShiftEnter: model.config.sendWithShiftEnter
|
|
108
|
-
},
|
|
109
|
-
attachments: structuredClone(message.attachments ?? []),
|
|
110
|
-
mentions: message.mentions
|
|
111
|
-
});
|
|
112
|
-
model.addEditionModel(message.id, inputModel);
|
|
113
|
-
setEdit(true);
|
|
114
|
-
};
|
|
72
|
+
setRenderedDelegate(props.message.renderedDelegate);
|
|
73
|
+
}
|
|
115
74
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
75
|
+
props.message.changed.connect(messageChanged);
|
|
76
|
+
|
|
77
|
+
// Initialize the message when the message is re-rendered.
|
|
78
|
+
// FIX ? This seems to be required for outofband change, to get the new value,
|
|
79
|
+
// even if when an outofband change occurs, all the messages are deleted and
|
|
80
|
+
// recreated.
|
|
81
|
+
setMessage(props.message.content);
|
|
82
|
+
setRenderedDelegate(props.message.renderedDelegate);
|
|
83
|
+
return () => {
|
|
84
|
+
props.message.changed.disconnect(messageChanged);
|
|
120
85
|
};
|
|
86
|
+
}, [props.message]);
|
|
87
|
+
|
|
88
|
+
// Create an input model only if the message is edited.
|
|
89
|
+
const startEdition = useCallback((): void => {
|
|
90
|
+
if (!canEdit || !(typeof message.body === 'string')) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
let body = message.body;
|
|
94
|
+
message.mentions?.forEach(user => {
|
|
95
|
+
body = replaceSpanToMention(body, user);
|
|
96
|
+
});
|
|
97
|
+
const inputModel = new InputModel({
|
|
98
|
+
chatContext: model.createChatContext(),
|
|
99
|
+
onSend: (input: string, model?: IInputModel) =>
|
|
100
|
+
updateMessage(message.id, input, model),
|
|
101
|
+
onCancel: () => cancelEdition(),
|
|
102
|
+
value: body,
|
|
103
|
+
activeCellManager: model.activeCellManager,
|
|
104
|
+
selectionWatcher: model.selectionWatcher,
|
|
105
|
+
documentManager: model.documentManager,
|
|
106
|
+
config: {
|
|
107
|
+
sendWithShiftEnter: model.config.sendWithShiftEnter
|
|
108
|
+
},
|
|
109
|
+
attachments: structuredClone(message.attachments ?? []),
|
|
110
|
+
mentions: message.mentions
|
|
111
|
+
});
|
|
112
|
+
model.addEditionModel(message.id, inputModel);
|
|
113
|
+
setEdit(true);
|
|
114
|
+
}, [canEdit, model, message]);
|
|
121
115
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
116
|
+
// Cancel the current edition of the message.
|
|
117
|
+
const cancelEdition = useCallback((): void => {
|
|
118
|
+
model.getEditionModel(message.id)?.dispose();
|
|
119
|
+
setEdit(false);
|
|
120
|
+
}, [model, message]);
|
|
121
|
+
|
|
122
|
+
// Update the content of the message.
|
|
123
|
+
const updateMessage = useCallback(
|
|
124
|
+
(id: string, input: string, inputModel?: IInputModel): void => {
|
|
128
125
|
if (!canEdit || !inputModel) {
|
|
129
126
|
return;
|
|
130
127
|
}
|
|
@@ -136,45 +133,55 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
|
|
136
133
|
model.updateMessage!(id, updatedMessage);
|
|
137
134
|
model.getEditionModel(message.id)?.dispose();
|
|
138
135
|
setEdit(false);
|
|
139
|
-
}
|
|
136
|
+
},
|
|
137
|
+
[model, message, canEdit]
|
|
138
|
+
);
|
|
140
139
|
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
// Delete the message.
|
|
141
|
+
const deleteMessage = useCallback(
|
|
142
|
+
(id: string): void => {
|
|
143
143
|
if (!canDelete) {
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
146
|
model.deleteMessage!(id);
|
|
147
|
-
}
|
|
147
|
+
},
|
|
148
|
+
[model, canDelete]
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (deleted) {
|
|
153
|
+
renderedDelegate.resolve();
|
|
154
|
+
}
|
|
155
|
+
}, [deleted, renderedDelegate]);
|
|
148
156
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
) : (
|
|
153
|
-
<div
|
|
154
|
-
ref={ref}
|
|
155
|
-
data-index={props.index}
|
|
156
|
-
className={MESSAGE_CONTAINER_CLASS}
|
|
157
|
-
>
|
|
158
|
-
{edit && canEdit && model.getEditionModel(message.id) ? (
|
|
159
|
-
<ChatInput
|
|
160
|
-
onCancel={() => cancelEdition()}
|
|
161
|
-
model={model.getEditionModel(message.id)!}
|
|
162
|
-
edit={true}
|
|
163
|
-
/>
|
|
164
|
-
) : (
|
|
165
|
-
<MessageRenderer
|
|
166
|
-
message={message}
|
|
167
|
-
edit={canEdit ? startEdition : undefined}
|
|
168
|
-
delete={canDelete ? () => deleteMessage(message.id) : undefined}
|
|
169
|
-
rendered={props.renderedPromise}
|
|
170
|
-
/>
|
|
171
|
-
)}
|
|
172
|
-
{message.attachments && !edit && (
|
|
173
|
-
// Display the attachments only if message is not edited, otherwise the
|
|
174
|
-
// input component display them.
|
|
175
|
-
<AttachmentPreviewList attachments={message.attachments} />
|
|
176
|
-
)}
|
|
177
|
-
</div>
|
|
178
|
-
);
|
|
157
|
+
// Empty if the message has been deleted.
|
|
158
|
+
if (deleted) {
|
|
159
|
+
return <div data-index={props.index}></div>;
|
|
179
160
|
}
|
|
180
|
-
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div data-index={props.index} className={MESSAGE_CONTAINER_CLASS}>
|
|
164
|
+
{edit && canEdit && model.getEditionModel(message.id) ? (
|
|
165
|
+
<ChatInput
|
|
166
|
+
onCancel={() => cancelEdition()}
|
|
167
|
+
model={model.getEditionModel(message.id)!}
|
|
168
|
+
edit={true}
|
|
169
|
+
/>
|
|
170
|
+
) : (
|
|
171
|
+
<MessageRenderer
|
|
172
|
+
message={message}
|
|
173
|
+
edit={canEdit ? startEdition : undefined}
|
|
174
|
+
delete={canDelete ? () => deleteMessage(message.id) : undefined}
|
|
175
|
+
rendered={renderedDelegate}
|
|
176
|
+
/>
|
|
177
|
+
)}
|
|
178
|
+
{message.attachments && !edit && (
|
|
179
|
+
// Display the attachments only if message is not edited, otherwise the
|
|
180
|
+
// input component display them.
|
|
181
|
+
<AttachmentPreviewList attachments={message.attachments} />
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const ChatMessage = React.memo(ChatMessageBase);
|
|
@@ -3,14 +3,13 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { PromiseDelegate } from '@lumino/coreutils';
|
|
7
6
|
import { Box } from '@mui/material';
|
|
8
7
|
import clsx from 'clsx';
|
|
9
8
|
import React, { useEffect, useState, useRef } from 'react';
|
|
10
9
|
|
|
11
10
|
import { MessageFooterComponent } from './footer';
|
|
12
11
|
import { ChatMessageHeader } from './header';
|
|
13
|
-
import { ChatMessage } from './message';
|
|
12
|
+
import { ChatMessage, MESSAGE_CONTAINER_CLASS } from './message';
|
|
14
13
|
import { MessagePreambleComponent } from './preamble';
|
|
15
14
|
import { Navigation } from './navigation';
|
|
16
15
|
import { WelcomeMessage } from './welcome';
|
|
@@ -44,10 +43,6 @@ export function ChatMessages(): JSX.Element {
|
|
|
44
43
|
model.config.showDeleted ?? false
|
|
45
44
|
);
|
|
46
45
|
|
|
47
|
-
// The list of message DOM and their rendered promises.
|
|
48
|
-
const listRef = useRef<(HTMLDivElement | null)[]>([]);
|
|
49
|
-
const renderedPromise = useRef<PromiseDelegate<void>[]>([]);
|
|
50
|
-
|
|
51
46
|
/**
|
|
52
47
|
* Effect: fetch history and config on initial render
|
|
53
48
|
*/
|
|
@@ -159,9 +154,12 @@ export function ChatMessages(): JSX.Element {
|
|
|
159
154
|
*/
|
|
160
155
|
useEffect(() => {
|
|
161
156
|
const observer = new IntersectionObserver(entries => {
|
|
162
|
-
// Used on first rendering, to ensure all the
|
|
157
|
+
// Used on first rendering, to ensure all the messages have been rendered once.
|
|
163
158
|
if (!allRendered) {
|
|
164
|
-
|
|
159
|
+
const promises = (
|
|
160
|
+
showDeleted ? messages : messages.filter(message => !message.deleted)
|
|
161
|
+
).map(msg => msg.renderedDelegate.promise);
|
|
162
|
+
Promise.all(promises).then(() => {
|
|
165
163
|
setAllRendered(true);
|
|
166
164
|
});
|
|
167
165
|
}
|
|
@@ -191,7 +189,7 @@ export function ChatMessages(): JSX.Element {
|
|
|
191
189
|
model.messagesInViewport = inViewport;
|
|
192
190
|
|
|
193
191
|
// Ensure that all messages are rendered before updating unread messages, otherwise
|
|
194
|
-
// it can lead to wrong assumption
|
|
192
|
+
// it can lead to wrong assumption, because more messages are in the viewport
|
|
195
193
|
// before they are rendered.
|
|
196
194
|
if (allRendered && unreadModified) {
|
|
197
195
|
model.unreadMessages = unread;
|
|
@@ -201,20 +199,16 @@ export function ChatMessages(): JSX.Element {
|
|
|
201
199
|
/**
|
|
202
200
|
* Observe the messages.
|
|
203
201
|
*/
|
|
204
|
-
|
|
205
|
-
|
|
202
|
+
refMsgBox.current
|
|
203
|
+
?.querySelectorAll(`.${MESSAGE_CONTAINER_CLASS}`)
|
|
204
|
+
.forEach(item => {
|
|
206
205
|
observer.observe(item);
|
|
207
|
-
}
|
|
208
|
-
});
|
|
206
|
+
});
|
|
209
207
|
|
|
210
208
|
return () => {
|
|
211
|
-
|
|
212
|
-
if (item) {
|
|
213
|
-
observer.unobserve(item);
|
|
214
|
-
}
|
|
215
|
-
});
|
|
209
|
+
observer.disconnect();
|
|
216
210
|
};
|
|
217
|
-
}, [messages, allRendered]);
|
|
211
|
+
}, [messages, showDeleted, allRendered]);
|
|
218
212
|
|
|
219
213
|
const horizontalPadding = area === 'main' ? 8 : 4;
|
|
220
214
|
return (
|
|
@@ -240,7 +234,6 @@ export function ChatMessages(): JSX.Element {
|
|
|
240
234
|
? messages
|
|
241
235
|
: messages.filter(message => !message.deleted)
|
|
242
236
|
).map((message, i) => {
|
|
243
|
-
renderedPromise.current[i] = new PromiseDelegate();
|
|
244
237
|
const isCurrentUser =
|
|
245
238
|
model.user !== undefined &&
|
|
246
239
|
model.user.username === message.sender.username;
|
|
@@ -269,12 +262,7 @@ export function ChatMessages(): JSX.Element {
|
|
|
269
262
|
{messagePreambleRegistry && (
|
|
270
263
|
<MessagePreambleComponent message={message} />
|
|
271
264
|
)}
|
|
272
|
-
<ChatMessage
|
|
273
|
-
message={message}
|
|
274
|
-
index={i}
|
|
275
|
-
renderedPromise={renderedPromise.current[i]}
|
|
276
|
-
ref={el => (listRef.current[i] = el)}
|
|
277
|
-
/>
|
|
265
|
+
<ChatMessage message={message} index={i} />
|
|
278
266
|
{messageFooterRegistry && (
|
|
279
267
|
<MessageFooterComponent message={message} />
|
|
280
268
|
)}
|
package/src/message.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
6
7
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
7
8
|
import {
|
|
8
9
|
IAttachment,
|
|
@@ -83,14 +84,42 @@ export class Message implements IMessage {
|
|
|
83
84
|
return this._changed;
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
/**
|
|
88
|
+
* A promise delegate that resolve when the message is rendered.
|
|
89
|
+
*/
|
|
90
|
+
get renderedDelegate(): PromiseDelegate<void> {
|
|
91
|
+
return this._rendered;
|
|
92
|
+
}
|
|
93
|
+
|
|
86
94
|
/**
|
|
87
95
|
* Update one or several fields of the message.
|
|
88
96
|
*/
|
|
89
97
|
update(updated: Partial<IMessageContent>) {
|
|
98
|
+
// Check if the message will be rendered again.
|
|
99
|
+
const triggerRerender = new Set([
|
|
100
|
+
'body',
|
|
101
|
+
'deleted',
|
|
102
|
+
'mentions',
|
|
103
|
+
'mime_model'
|
|
104
|
+
] as (keyof IMessageContent)[]);
|
|
105
|
+
const updatedAttributes = Object.keys(updated) as (keyof IMessageContent)[];
|
|
106
|
+
const shouldRerender = updatedAttributes.some(
|
|
107
|
+
attribute =>
|
|
108
|
+
triggerRerender.has(attribute) &&
|
|
109
|
+
updated[attribute] !== this._content[attribute]
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Update the message content.
|
|
90
113
|
this._content = { ...this._content, ...updated };
|
|
114
|
+
|
|
115
|
+
// Update the promise delegate if the message will be rendered again.
|
|
116
|
+
if (shouldRerender) {
|
|
117
|
+
this._rendered = new PromiseDelegate<void>();
|
|
118
|
+
}
|
|
91
119
|
this._changed.emit();
|
|
92
120
|
}
|
|
93
121
|
|
|
94
122
|
private _content: IMessageContent;
|
|
95
123
|
private _changed = new Signal<IMessage, void>(this);
|
|
124
|
+
private _rendered = new PromiseDelegate<void>();
|
|
96
125
|
}
|
package/src/model.ts
CHANGED
|
@@ -175,6 +175,11 @@ export interface IChatModel extends IDisposable {
|
|
|
175
175
|
*/
|
|
176
176
|
isDisposed: boolean;
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* A signal emitting when disposing of the model.
|
|
180
|
+
*/
|
|
181
|
+
readonly disposed: ISignal<IChatModel, void>;
|
|
182
|
+
|
|
178
183
|
/**
|
|
179
184
|
* Function to call when a message is received.
|
|
180
185
|
*
|
|
@@ -306,7 +311,10 @@ export abstract class AbstractChatModel implements IChatModel {
|
|
|
306
311
|
this._name = value;
|
|
307
312
|
}
|
|
308
313
|
|
|
309
|
-
|
|
314
|
+
/**
|
|
315
|
+
* A signal emitting when disposing of the model.
|
|
316
|
+
*/
|
|
317
|
+
get disposed(): ISignal<IChatModel, void> {
|
|
310
318
|
return this._disposed;
|
|
311
319
|
}
|
|
312
320
|
|
|
@@ -769,7 +777,7 @@ export abstract class AbstractChatModel implements IChatModel {
|
|
|
769
777
|
protected _trans: TranslationBundle;
|
|
770
778
|
private _readyDelegate = new PromiseDelegate<void>();
|
|
771
779
|
private _inputModel: IInputModel;
|
|
772
|
-
private _disposed = new Signal<
|
|
780
|
+
private _disposed = new Signal<IChatModel, void>(this);
|
|
773
781
|
private _isDisposed = false;
|
|
774
782
|
private _commands?: CommandRegistry;
|
|
775
783
|
private _activeCellManager: IActiveCellManager | null;
|
package/src/types.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
6
7
|
import { ISignal } from '@lumino/signaling';
|
|
7
8
|
import { IRenderMime } from '@jupyterlab/rendermime';
|
|
8
9
|
|
|
@@ -157,6 +158,10 @@ export interface IMessage extends IMessageContent {
|
|
|
157
158
|
* A signal emitting when the message has been updated.
|
|
158
159
|
*/
|
|
159
160
|
changed: ISignal<IMessage, void>;
|
|
161
|
+
/**
|
|
162
|
+
* A promise delegate that resolve when the message is rendered.
|
|
163
|
+
*/
|
|
164
|
+
renderedDelegate: PromiseDelegate<void>;
|
|
160
165
|
}
|
|
161
166
|
|
|
162
167
|
/**
|