@jupyter/chat 0.22.0 → 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/utils.js +2 -2
- 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/utils.ts +2 -2
- package/src/widgets/multichat-panel.tsx +62 -11
- package/style/chat.css +8 -4
|
@@ -11,6 +11,76 @@ describe('MultiChatPanel', () => {
|
|
|
11
11
|
beforeEach(() => {
|
|
12
12
|
rmRegistry = new RenderMimeRegistry();
|
|
13
13
|
});
|
|
14
|
+
describe('unsetLoadedModel', () => {
|
|
15
|
+
it('should dispose the model by default', async () => {
|
|
16
|
+
const panel = new MultiChatPanel({ rmRegistry });
|
|
17
|
+
const { MockChatModel } = await import('./mocks');
|
|
18
|
+
const model = new MockChatModel();
|
|
19
|
+
panel.open({ model, displayName: 'test-chat' });
|
|
20
|
+
panel.unsetLoadedModel('test-chat');
|
|
21
|
+
expect(model.isDisposed).toBe(true);
|
|
22
|
+
panel.dispose();
|
|
23
|
+
});
|
|
24
|
+
it('should not dispose the model when dispose=false', async () => {
|
|
25
|
+
const panel = new MultiChatPanel({ rmRegistry });
|
|
26
|
+
const { MockChatModel } = await import('./mocks');
|
|
27
|
+
const model = new MockChatModel();
|
|
28
|
+
panel.open({ model, displayName: 'test-chat' });
|
|
29
|
+
panel.unsetLoadedModel('test-chat', false);
|
|
30
|
+
expect(model.isDisposed).toBe(false);
|
|
31
|
+
model.dispose();
|
|
32
|
+
panel.dispose();
|
|
33
|
+
});
|
|
34
|
+
it('should dispose the model when clicking the close button', async () => {
|
|
35
|
+
const panel = new MultiChatPanel({ rmRegistry });
|
|
36
|
+
const { MockChatModel } = await import('./mocks');
|
|
37
|
+
const model = new MockChatModel();
|
|
38
|
+
panel.open({ model, displayName: 'test-chat' });
|
|
39
|
+
const toolbar = panel.current.toolbar;
|
|
40
|
+
const names = Array.from(toolbar.names());
|
|
41
|
+
const closeIdx = names.indexOf('close');
|
|
42
|
+
expect(closeIdx).toBeGreaterThan(-1);
|
|
43
|
+
const closeButton = toolbar.layout.widgets[closeIdx];
|
|
44
|
+
closeButton.onClick();
|
|
45
|
+
expect(model.isDisposed).toBe(true);
|
|
46
|
+
panel.dispose();
|
|
47
|
+
});
|
|
48
|
+
it('should remove the model from loaded models regardless of dispose flag', async () => {
|
|
49
|
+
const panel = new MultiChatPanel({ rmRegistry });
|
|
50
|
+
const { MockChatModel } = await import('./mocks');
|
|
51
|
+
const model = new MockChatModel();
|
|
52
|
+
panel.open({ model, displayName: 'test-chat' });
|
|
53
|
+
panel.unsetLoadedModel('test-chat', false);
|
|
54
|
+
expect(panel.getLoadedModel('test-chat')).toBeUndefined();
|
|
55
|
+
model.dispose();
|
|
56
|
+
panel.dispose();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('openInMain', () => {
|
|
60
|
+
it('should not dispose the model when moving to main area', async () => {
|
|
61
|
+
const { MockChatModel } = await import('./mocks');
|
|
62
|
+
const model = new MockChatModel();
|
|
63
|
+
const openInMain = jest.fn().mockResolvedValue(true);
|
|
64
|
+
const panel = new MultiChatPanel({ rmRegistry, openInMain });
|
|
65
|
+
panel.open({ model, displayName: 'test-chat' });
|
|
66
|
+
// Find the 'moveMain' toolbar button by name and invoke its click handler directly.
|
|
67
|
+
// ToolbarButton stores the onClick handler independently of DOM/React rendering,
|
|
68
|
+
// so this works without attaching the widget to the document.
|
|
69
|
+
const toolbar = panel.current.toolbar;
|
|
70
|
+
const names = Array.from(toolbar.names());
|
|
71
|
+
const moveMainIdx = names.indexOf('moveMain');
|
|
72
|
+
expect(moveMainIdx).toBeGreaterThan(-1);
|
|
73
|
+
const moveButton = toolbar.layout.widgets[moveMainIdx];
|
|
74
|
+
moveButton.onClick();
|
|
75
|
+
// Wait for the async onClick handler: openInMain resolves, then onClose is called.
|
|
76
|
+
await Promise.resolve();
|
|
77
|
+
expect(openInMain).toHaveBeenCalledWith(model.name);
|
|
78
|
+
expect(model.isDisposed).toBe(false);
|
|
79
|
+
expect(panel.getLoadedModel('test-chat')).toBeUndefined();
|
|
80
|
+
model.dispose();
|
|
81
|
+
panel.dispose();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
14
84
|
describe('placeholderFactory', () => {
|
|
15
85
|
it('should use defaultPlaceholder when no factory is provided', () => {
|
|
16
86
|
const panel = new MultiChatPanel({ rmRegistry });
|
|
@@ -108,9 +178,20 @@ describe('MultiChatPanel', () => {
|
|
|
108
178
|
const { MockChatModel } = await import('./mocks');
|
|
109
179
|
const model = new MockChatModel();
|
|
110
180
|
panel.open({ model, displayName: 'test-chat' });
|
|
111
|
-
panel.
|
|
181
|
+
panel.unsetLoadedModel('test-chat');
|
|
112
182
|
expect(factory.create).toHaveBeenCalledTimes(2);
|
|
113
183
|
panel.dispose();
|
|
114
184
|
});
|
|
115
185
|
});
|
|
186
|
+
describe('side panel toolbar', () => {
|
|
187
|
+
it('should include the responsive toolbar opener for overflow actions', async () => {
|
|
188
|
+
var _a;
|
|
189
|
+
const panel = new MultiChatPanel({ rmRegistry });
|
|
190
|
+
const { MockChatModel } = await import('./mocks');
|
|
191
|
+
panel.open({ model: new MockChatModel(), displayName: 'test-chat' });
|
|
192
|
+
const opener = (_a = panel.current) === null || _a === void 0 ? void 0 : _a.toolbar.node.querySelector('.jp-Toolbar-responsive-opener');
|
|
193
|
+
expect(opener).not.toBeNull();
|
|
194
|
+
panel.dispose();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
116
197
|
});
|
package/lib/components/chat.d.ts
CHANGED
package/lib/components/chat.js
CHANGED
|
@@ -6,7 +6,7 @@ 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, { useEffect, useState } from 'react';
|
|
9
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
10
10
|
import { ChatInput, InputToolbarRegistry } from './input';
|
|
11
11
|
import { JlThemeProvider } from './jl-theme-provider';
|
|
12
12
|
import { ChatMessages } from './messages';
|
|
@@ -15,10 +15,6 @@ import { ChatReactContext } from '../context';
|
|
|
15
15
|
export function ChatBody(props) {
|
|
16
16
|
const { model } = props;
|
|
17
17
|
const [writers, setWriters] = useState([]);
|
|
18
|
-
let { inputToolbarRegistry } = props;
|
|
19
|
-
if (!inputToolbarRegistry) {
|
|
20
|
-
inputToolbarRegistry = InputToolbarRegistry.defaultToolbarRegistry();
|
|
21
|
-
}
|
|
22
18
|
/**
|
|
23
19
|
* Handle the changes in the writers list.
|
|
24
20
|
*/
|
|
@@ -41,10 +37,13 @@ export function ChatBody(props) {
|
|
|
41
37
|
};
|
|
42
38
|
}, [model]);
|
|
43
39
|
const horizontalPadding = 4;
|
|
44
|
-
const contextValue = {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
const contextValue = useMemo(() => {
|
|
41
|
+
var _a;
|
|
42
|
+
return ({
|
|
43
|
+
...props,
|
|
44
|
+
inputToolbarRegistry: (_a = props.inputToolbarRegistry) !== null && _a !== void 0 ? _a : InputToolbarRegistry.defaultToolbarRegistry()
|
|
45
|
+
});
|
|
46
|
+
}, [props]);
|
|
48
47
|
return (React.createElement(ChatReactContext.Provider, { value: contextValue },
|
|
49
48
|
React.createElement(ChatMessages, null),
|
|
50
49
|
React.createElement(ChatInput, { sx: {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import CheckIcon from '@mui/icons-material/Check';
|
|
6
6
|
import React, { useEffect, useState } from 'react';
|
|
7
|
+
import { submitInputMessage } from '../submit-message';
|
|
7
8
|
import { TooltippedIconButton } from '../../mui-extras';
|
|
8
9
|
import { useTranslator } from '../../../context';
|
|
9
10
|
const SAVE_EDIT_BUTTON_CLASS = 'jp-chat-save-edit-button';
|
|
@@ -35,8 +36,7 @@ export function SaveEditButton(props) {
|
|
|
35
36
|
};
|
|
36
37
|
}, [model]);
|
|
37
38
|
async function save() {
|
|
38
|
-
await (
|
|
39
|
-
model.send(model.value);
|
|
39
|
+
await submitInputMessage({ model, chatCommandRegistry });
|
|
40
40
|
}
|
|
41
41
|
return (React.createElement(TooltippedIconButton, { onClick: save, tooltip: tooltip, disabled: disabled, buttonProps: {
|
|
42
42
|
title: tooltip,
|
|
@@ -6,6 +6,7 @@ import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
|
|
6
6
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|
7
7
|
import { Box, Menu, MenuItem, Typography } from '@mui/material';
|
|
8
8
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
9
|
+
import { submitInputMessage } from '../submit-message';
|
|
9
10
|
import { TooltippedIconButton } from '../../mui-extras';
|
|
10
11
|
import { useTranslator } from '../../../context';
|
|
11
12
|
import { includeSelectionIcon } from '../../../icons';
|
|
@@ -86,15 +87,9 @@ export function SendButton(props) {
|
|
|
86
87
|
};
|
|
87
88
|
}, [activeCellManager, selectionWatcher, showSendWithSelection]);
|
|
88
89
|
async function send() {
|
|
89
|
-
|
|
90
|
-
await (chatCommandRegistry === null || chatCommandRegistry === void 0 ? void 0 : chatCommandRegistry.onSubmit(model));
|
|
91
|
-
const body = model.value;
|
|
92
|
-
model.value = '';
|
|
93
|
-
model.send(body);
|
|
94
|
-
model.focus();
|
|
90
|
+
await submitInputMessage({ model, chatCommandRegistry });
|
|
95
91
|
}
|
|
96
92
|
async function sendWithSelection() {
|
|
97
|
-
await (chatCommandRegistry === null || chatCommandRegistry === void 0 ? void 0 : chatCommandRegistry.onSubmit(model));
|
|
98
93
|
let source = '';
|
|
99
94
|
let language;
|
|
100
95
|
if (selectionWatcher === null || selectionWatcher === void 0 ? void 0 : selectionWatcher.selection) {
|
|
@@ -110,10 +105,8 @@ export function SendButton(props) {
|
|
|
110
105
|
if (source) {
|
|
111
106
|
body += `\n\n\`\`\`${language !== null && language !== void 0 ? language : ''}\n${source}\n\`\`\`\n`;
|
|
112
107
|
}
|
|
113
|
-
model.value = '';
|
|
114
108
|
closeMenu();
|
|
115
|
-
model
|
|
116
|
-
model.focus();
|
|
109
|
+
await submitInputMessage({ model, chatCommandRegistry, body });
|
|
117
110
|
}
|
|
118
111
|
return (React.createElement(Box, { sx: { display: 'inline-flex', alignItems: 'center', gap: '1px' } },
|
|
119
112
|
React.createElement(TooltippedIconButton, { onClick: send, tooltip: tooltip, disabled: disabled, sx: {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { Autocomplete, Box, TextField } from '@mui/material';
|
|
6
6
|
import clsx from 'clsx';
|
|
7
7
|
import React, { useEffect, useRef, useState } from 'react';
|
|
8
|
+
import { submitInputMessage } from './submit-message';
|
|
8
9
|
import { useChatCommands } from './use-chat-commands';
|
|
9
10
|
import { AttachmentPreviewList } from '../attachments';
|
|
10
11
|
import { useChatContext, useTranslator } from '../../context';
|
|
@@ -130,9 +131,7 @@ export function ChatInput(props) {
|
|
|
130
131
|
}
|
|
131
132
|
// Finally, send the message when all other conditions are met.
|
|
132
133
|
if (isSendCombination) {
|
|
133
|
-
|
|
134
|
-
await (chatCommandRegistry === null || chatCommandRegistry === void 0 ? void 0 : chatCommandRegistry.onSubmit(model));
|
|
135
|
-
model.send(model.value);
|
|
134
|
+
await submitInputMessage({ model, chatCommandRegistry });
|
|
136
135
|
event.stopPropagation();
|
|
137
136
|
event.preventDefault();
|
|
138
137
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { IInputModel } from '../../input-model';
|
|
2
|
+
import { IChatCommandRegistry } from '../../registers';
|
|
3
|
+
/**
|
|
4
|
+
* Submit the current input value as a chat message.
|
|
5
|
+
*/
|
|
6
|
+
export declare function submitInputMessage(options: submitInputMessage.IOptions): Promise<void>;
|
|
7
|
+
export declare namespace submitInputMessage {
|
|
8
|
+
interface IOptions {
|
|
9
|
+
/**
|
|
10
|
+
* The input model containing the message to send.
|
|
11
|
+
*/
|
|
12
|
+
model: IInputModel;
|
|
13
|
+
/**
|
|
14
|
+
* Optional chat command registry used to preprocess message content.
|
|
15
|
+
*/
|
|
16
|
+
chatCommandRegistry?: IChatCommandRegistry;
|
|
17
|
+
/**
|
|
18
|
+
* Optional message body to send instead of the current model value.
|
|
19
|
+
*/
|
|
20
|
+
body?: string;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Submit the current input value as a chat message.
|
|
7
|
+
*/
|
|
8
|
+
export async function submitInputMessage(options) {
|
|
9
|
+
const { model, chatCommandRegistry, body } = options;
|
|
10
|
+
await (chatCommandRegistry === null || chatCommandRegistry === void 0 ? void 0 : chatCommandRegistry.onSubmit(model));
|
|
11
|
+
model.send(body !== null && body !== void 0 ? body : model.value);
|
|
12
|
+
model.focus();
|
|
13
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import React from 'react';
|
|
2
2
|
import { IMessage } from '../../types';
|
|
3
3
|
/**
|
|
4
4
|
* The message header props.
|
|
@@ -16,5 +16,6 @@ type ChatMessageHeaderProps = {
|
|
|
16
16
|
/**
|
|
17
17
|
* The message header component.
|
|
18
18
|
*/
|
|
19
|
-
export declare function
|
|
19
|
+
export declare function ChatMessageHeaderBase(props: ChatMessageHeaderProps): JSX.Element;
|
|
20
|
+
export declare const ChatMessageHeader: React.MemoExoticComponent<typeof ChatMessageHeaderBase>;
|
|
20
21
|
export {};
|
|
@@ -11,7 +11,7 @@ const MESSAGE_TIME_CLASS = 'jp-chat-message-time';
|
|
|
11
11
|
/**
|
|
12
12
|
* The message header component.
|
|
13
13
|
*/
|
|
14
|
-
export function
|
|
14
|
+
export function ChatMessageHeaderBase(props) {
|
|
15
15
|
var _a, _b;
|
|
16
16
|
const trans = useTranslator();
|
|
17
17
|
const [message, setMessage] = useState(props.message.content);
|
|
@@ -100,3 +100,4 @@ export function ChatMessageHeader(props) {
|
|
|
100
100
|
fontWeight: 300
|
|
101
101
|
}, title: message.raw_time ? trans.__('Unverified time') : '' }, `${datetime[message.time]}${message.raw_time ? '*' : ''}`)))));
|
|
102
102
|
}
|
|
103
|
+
export const ChatMessageHeader = React.memo(ChatMessageHeaderBase);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { PromiseDelegate } from '@lumino/coreutils';
|
|
2
1
|
import React from 'react';
|
|
3
2
|
import { IMessage } from '../../types';
|
|
3
|
+
export declare const MESSAGE_CONTAINER_CLASS = "jp-chat-message-container";
|
|
4
4
|
/**
|
|
5
5
|
* The message component props.
|
|
6
6
|
*/
|
|
@@ -13,13 +13,10 @@ type ChatMessageProps = {
|
|
|
13
13
|
* The index of the message in the list.
|
|
14
14
|
*/
|
|
15
15
|
index: number;
|
|
16
|
-
/**
|
|
17
|
-
* The promise to resolve when the message is rendered.
|
|
18
|
-
*/
|
|
19
|
-
renderedPromise: PromiseDelegate<void>;
|
|
20
16
|
};
|
|
21
17
|
/**
|
|
22
18
|
* The message component body.
|
|
23
19
|
*/
|
|
24
|
-
export declare
|
|
20
|
+
export declare function ChatMessageBase(props: ChatMessageProps): JSX.Element;
|
|
21
|
+
export declare const ChatMessage: React.MemoExoticComponent<typeof ChatMessageBase>;
|
|
25
22
|
export {};
|
|
@@ -2,20 +2,21 @@
|
|
|
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, { useCallback, useEffect, useState } from 'react';
|
|
6
6
|
import { MessageRenderer } from './message-renderer';
|
|
7
7
|
import { AttachmentPreviewList } from '../attachments';
|
|
8
8
|
import { ChatInput } from '../input';
|
|
9
9
|
import { useChatContext } from '../../context';
|
|
10
10
|
import { InputModel } from '../../input-model';
|
|
11
11
|
import { replaceSpanToMention } from '../../utils';
|
|
12
|
-
const MESSAGE_CONTAINER_CLASS = 'jp-chat-message-container';
|
|
12
|
+
export const MESSAGE_CONTAINER_CLASS = 'jp-chat-message-container';
|
|
13
13
|
/**
|
|
14
14
|
* The message component body.
|
|
15
15
|
*/
|
|
16
|
-
export
|
|
16
|
+
export function ChatMessageBase(props) {
|
|
17
17
|
const { model } = useChatContext();
|
|
18
18
|
const [message, setMessage] = useState(props.message.content);
|
|
19
|
+
const [renderedDelegate, setRenderedDelegate] = useState(props.message.renderedDelegate);
|
|
19
20
|
const [edit, setEdit] = useState(false);
|
|
20
21
|
const [deleted, setDeleted] = useState(false);
|
|
21
22
|
const [canEdit, setCanEdit] = useState(false);
|
|
@@ -44,6 +45,7 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
44
45
|
useEffect(() => {
|
|
45
46
|
function messageChanged() {
|
|
46
47
|
setMessage(props.message.content);
|
|
48
|
+
setRenderedDelegate(props.message.renderedDelegate);
|
|
47
49
|
}
|
|
48
50
|
props.message.changed.connect(messageChanged);
|
|
49
51
|
// Initialize the message when the message is re-rendered.
|
|
@@ -51,12 +53,13 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
51
53
|
// even if when an outofband change occurs, all the messages are deleted and
|
|
52
54
|
// recreated.
|
|
53
55
|
setMessage(props.message.content);
|
|
56
|
+
setRenderedDelegate(props.message.renderedDelegate);
|
|
54
57
|
return () => {
|
|
55
58
|
props.message.changed.disconnect(messageChanged);
|
|
56
59
|
};
|
|
57
60
|
}, [props.message]);
|
|
58
61
|
// Create an input model only if the message is edited.
|
|
59
|
-
const startEdition = () => {
|
|
62
|
+
const startEdition = useCallback(() => {
|
|
60
63
|
var _a, _b;
|
|
61
64
|
if (!canEdit || !(typeof message.body === 'string')) {
|
|
62
65
|
return;
|
|
@@ -81,15 +84,15 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
81
84
|
});
|
|
82
85
|
model.addEditionModel(message.id, inputModel);
|
|
83
86
|
setEdit(true);
|
|
84
|
-
};
|
|
87
|
+
}, [canEdit, model, message]);
|
|
85
88
|
// Cancel the current edition of the message.
|
|
86
|
-
const cancelEdition = () => {
|
|
89
|
+
const cancelEdition = useCallback(() => {
|
|
87
90
|
var _a;
|
|
88
91
|
(_a = model.getEditionModel(message.id)) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
89
92
|
setEdit(false);
|
|
90
|
-
};
|
|
93
|
+
}, [model, message]);
|
|
91
94
|
// Update the content of the message.
|
|
92
|
-
const updateMessage = (id, input, inputModel) => {
|
|
95
|
+
const updateMessage = useCallback((id, input, inputModel) => {
|
|
93
96
|
var _a;
|
|
94
97
|
if (!canEdit || !inputModel) {
|
|
95
98
|
return;
|
|
@@ -102,19 +105,28 @@ export const ChatMessage = forwardRef((props, ref) => {
|
|
|
102
105
|
model.updateMessage(id, updatedMessage);
|
|
103
106
|
(_a = model.getEditionModel(message.id)) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
104
107
|
setEdit(false);
|
|
105
|
-
};
|
|
108
|
+
}, [model, message, canEdit]);
|
|
106
109
|
// Delete the message.
|
|
107
|
-
const deleteMessage = (id) => {
|
|
110
|
+
const deleteMessage = useCallback((id) => {
|
|
108
111
|
if (!canDelete) {
|
|
109
112
|
return;
|
|
110
113
|
}
|
|
111
114
|
model.deleteMessage(id);
|
|
112
|
-
};
|
|
115
|
+
}, [model, canDelete]);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (deleted) {
|
|
118
|
+
renderedDelegate.resolve();
|
|
119
|
+
}
|
|
120
|
+
}, [deleted, renderedDelegate]);
|
|
113
121
|
// Empty if the message has been deleted.
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
if (deleted) {
|
|
123
|
+
return React.createElement("div", { "data-index": props.index });
|
|
124
|
+
}
|
|
125
|
+
return (React.createElement("div", { "data-index": props.index, className: MESSAGE_CONTAINER_CLASS },
|
|
126
|
+
edit && canEdit && model.getEditionModel(message.id) ? (React.createElement(ChatInput, { onCancel: () => cancelEdition(), model: model.getEditionModel(message.id), edit: true })) : (React.createElement(MessageRenderer, { message: message, edit: canEdit ? startEdition : undefined, delete: canDelete ? () => deleteMessage(message.id) : undefined, rendered: renderedDelegate })),
|
|
116
127
|
message.attachments && !edit && (
|
|
117
128
|
// Display the attachments only if message is not edited, otherwise the
|
|
118
129
|
// input component display them.
|
|
119
130
|
React.createElement(AttachmentPreviewList, { attachments: message.attachments }))));
|
|
120
|
-
}
|
|
131
|
+
}
|
|
132
|
+
export const ChatMessage = React.memo(ChatMessageBase);
|
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
* Copyright (c) Jupyter Development Team.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
-
import { PromiseDelegate } from '@lumino/coreutils';
|
|
6
5
|
import { Box } from '@mui/material';
|
|
7
6
|
import clsx from 'clsx';
|
|
8
7
|
import React, { useEffect, useState, useRef } from 'react';
|
|
9
8
|
import { MessageFooterComponent } from './footer';
|
|
10
9
|
import { ChatMessageHeader } from './header';
|
|
11
|
-
import { ChatMessage } from './message';
|
|
10
|
+
import { ChatMessage, MESSAGE_CONTAINER_CLASS } from './message';
|
|
12
11
|
import { MessagePreambleComponent } from './preamble';
|
|
13
12
|
import { Navigation } from './navigation';
|
|
14
13
|
import { WelcomeMessage } from './welcome';
|
|
@@ -29,9 +28,6 @@ export function ChatMessages() {
|
|
|
29
28
|
const refMsgBox = useRef(null);
|
|
30
29
|
const [allRendered, setAllRendered] = useState(false);
|
|
31
30
|
const [showDeleted, setShowDeleted] = useState((_a = model.config.showDeleted) !== null && _a !== void 0 ? _a : false);
|
|
32
|
-
// The list of message DOM and their rendered promises.
|
|
33
|
-
const listRef = useRef([]);
|
|
34
|
-
const renderedPromise = useRef([]);
|
|
35
31
|
/**
|
|
36
32
|
* Effect: fetch history and config on initial render
|
|
37
33
|
*/
|
|
@@ -128,11 +124,13 @@ export function ChatMessages() {
|
|
|
128
124
|
* Observe the messages to update the current viewport and the unread messages.
|
|
129
125
|
*/
|
|
130
126
|
useEffect(() => {
|
|
127
|
+
var _a;
|
|
131
128
|
const observer = new IntersectionObserver(entries => {
|
|
132
129
|
var _a;
|
|
133
|
-
// Used on first rendering, to ensure all the
|
|
130
|
+
// Used on first rendering, to ensure all the messages have been rendered once.
|
|
134
131
|
if (!allRendered) {
|
|
135
|
-
|
|
132
|
+
const promises = (showDeleted ? messages : messages.filter(message => !message.deleted)).map(msg => msg.renderedDelegate.promise);
|
|
133
|
+
Promise.all(promises).then(() => {
|
|
136
134
|
setAllRendered(true);
|
|
137
135
|
});
|
|
138
136
|
}
|
|
@@ -161,7 +159,7 @@ export function ChatMessages() {
|
|
|
161
159
|
});
|
|
162
160
|
model.messagesInViewport = inViewport;
|
|
163
161
|
// Ensure that all messages are rendered before updating unread messages, otherwise
|
|
164
|
-
// it can lead to wrong assumption
|
|
162
|
+
// it can lead to wrong assumption, because more messages are in the viewport
|
|
165
163
|
// before they are rendered.
|
|
166
164
|
if (allRendered && unreadModified) {
|
|
167
165
|
model.unreadMessages = unread;
|
|
@@ -170,19 +168,13 @@ export function ChatMessages() {
|
|
|
170
168
|
/**
|
|
171
169
|
* Observe the messages.
|
|
172
170
|
*/
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
observer.observe(item);
|
|
176
|
-
}
|
|
171
|
+
(_a = refMsgBox.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll(`.${MESSAGE_CONTAINER_CLASS}`).forEach(item => {
|
|
172
|
+
observer.observe(item);
|
|
177
173
|
});
|
|
178
174
|
return () => {
|
|
179
|
-
|
|
180
|
-
if (item) {
|
|
181
|
-
observer.unobserve(item);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
175
|
+
observer.disconnect();
|
|
184
176
|
};
|
|
185
|
-
}, [messages, allRendered]);
|
|
177
|
+
}, [messages, showDeleted, allRendered]);
|
|
186
178
|
const horizontalPadding = area === 'main' ? 8 : 4;
|
|
187
179
|
return (React.createElement(React.Fragment, null,
|
|
188
180
|
React.createElement(ScrollContainer, { ref: scrollContainerRef, sx: { flexGrow: 1 } },
|
|
@@ -199,7 +191,6 @@ export function ChatMessages() {
|
|
|
199
191
|
}, ref: refMsgBox, className: clsx(MESSAGES_BOX_CLASS) }, (showDeleted
|
|
200
192
|
? messages
|
|
201
193
|
: messages.filter(message => !message.deleted)).map((message, i) => {
|
|
202
|
-
renderedPromise.current[i] = new PromiseDelegate();
|
|
203
194
|
const isCurrentUser = model.user !== undefined &&
|
|
204
195
|
model.user.username === message.sender.username;
|
|
205
196
|
return (
|
|
@@ -215,7 +206,7 @@ export function ChatMessages() {
|
|
|
215
206
|
}, className: clsx(MESSAGE_CLASS, message.stacked ? MESSAGE_STACKED_CLASS : '') },
|
|
216
207
|
React.createElement(ChatMessageHeader, { message: message, isCurrentUser: isCurrentUser }),
|
|
217
208
|
messagePreambleRegistry && (React.createElement(MessagePreambleComponent, { message: message })),
|
|
218
|
-
React.createElement(ChatMessage, { message: message, index: i
|
|
209
|
+
React.createElement(ChatMessage, { message: message, index: i }),
|
|
219
210
|
messageFooterRegistry && (React.createElement(MessageFooterComponent, { message: message }))));
|
|
220
211
|
}))),
|
|
221
212
|
React.createElement(Navigation, { refMsgBox: refMsgBox, allRendered: allRendered })));
|
package/lib/context.d.ts
CHANGED
package/lib/message.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PromiseDelegate } from '@lumino/coreutils';
|
|
1
2
|
import { ISignal } from '@lumino/signaling';
|
|
2
3
|
import { IAttachment, IMessageContent, IMessage, IMessageMetadata, IUser, IMimeModelBody } from './types';
|
|
3
4
|
/**
|
|
@@ -34,10 +35,15 @@ export declare class Message implements IMessage {
|
|
|
34
35
|
* A signal emitting when the message has been updated.
|
|
35
36
|
*/
|
|
36
37
|
get changed(): ISignal<IMessage, void>;
|
|
38
|
+
/**
|
|
39
|
+
* A promise delegate that resolve when the message is rendered.
|
|
40
|
+
*/
|
|
41
|
+
get renderedDelegate(): PromiseDelegate<void>;
|
|
37
42
|
/**
|
|
38
43
|
* Update one or several fields of the message.
|
|
39
44
|
*/
|
|
40
45
|
update(updated: Partial<IMessageContent>): void;
|
|
41
46
|
private _content;
|
|
42
47
|
private _changed;
|
|
48
|
+
private _rendered;
|
|
43
49
|
}
|