@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.
Files changed (59) hide show
  1. package/lib/__tests__/multichat-panel.spec.js +82 -1
  2. package/lib/components/attachments.d.ts +0 -1
  3. package/lib/components/avatar.d.ts +0 -1
  4. package/lib/components/chat.d.ts +0 -1
  5. package/lib/components/chat.js +8 -9
  6. package/lib/components/code-blocks/code-toolbar.d.ts +0 -1
  7. package/lib/components/code-blocks/copy-button.d.ts +0 -1
  8. package/lib/components/input/buttons/attach-button.d.ts +0 -1
  9. package/lib/components/input/buttons/cancel-button.d.ts +0 -1
  10. package/lib/components/input/buttons/save-edit-button.d.ts +0 -1
  11. package/lib/components/input/buttons/save-edit-button.js +2 -2
  12. package/lib/components/input/buttons/send-button.d.ts +0 -1
  13. package/lib/components/input/buttons/send-button.js +3 -10
  14. package/lib/components/input/buttons/stop-button.d.ts +0 -1
  15. package/lib/components/input/chat-input.d.ts +0 -1
  16. package/lib/components/input/chat-input.js +2 -3
  17. package/lib/components/input/submit-message.d.ts +22 -0
  18. package/lib/components/input/submit-message.js +13 -0
  19. package/lib/components/messages/chat-body-placeholder.d.ts +0 -1
  20. package/lib/components/messages/footer.d.ts +0 -1
  21. package/lib/components/messages/header.d.ts +3 -2
  22. package/lib/components/messages/header.js +2 -1
  23. package/lib/components/messages/message.d.ts +3 -6
  24. package/lib/components/messages/message.js +26 -14
  25. package/lib/components/messages/messages.d.ts +0 -1
  26. package/lib/components/messages/messages.js +11 -20
  27. package/lib/components/messages/preamble.d.ts +0 -1
  28. package/lib/components/messages/toolbar.d.ts +0 -1
  29. package/lib/components/messages/welcome.d.ts +0 -1
  30. package/lib/components/mui-extras/tooltipped-icon-button.d.ts +0 -1
  31. package/lib/components/writing-indicator.d.ts +0 -1
  32. package/lib/context.d.ts +0 -1
  33. package/lib/message.d.ts +6 -0
  34. package/lib/message.js +23 -0
  35. package/lib/model.d.ts +8 -1
  36. package/lib/model.js +3 -0
  37. package/lib/registers/chat-commands.d.ts +0 -1
  38. package/lib/registers/footers.d.ts +0 -1
  39. package/lib/registers/preambles.d.ts +0 -1
  40. package/lib/tokens.d.ts +0 -1
  41. package/lib/types.d.ts +5 -0
  42. package/lib/widgets/chat-selector-popup.d.ts +0 -1
  43. package/lib/widgets/multichat-panel.d.ts +21 -5
  44. package/lib/widgets/multichat-panel.js +50 -10
  45. package/package.json +22 -22
  46. package/src/__tests__/multichat-panel.spec.ts +113 -1
  47. package/src/components/chat.tsx +10 -10
  48. package/src/components/input/buttons/save-edit-button.tsx +2 -2
  49. package/src/components/input/buttons/send-button.tsx +3 -13
  50. package/src/components/input/chat-input.tsx +2 -3
  51. package/src/components/input/submit-message.ts +38 -0
  52. package/src/components/messages/header.tsx +5 -1
  53. package/src/components/messages/message.tsx +129 -122
  54. package/src/components/messages/messages.tsx +14 -26
  55. package/src/message.ts +29 -0
  56. package/src/model.ts +10 -2
  57. package/src/types.ts +5 -0
  58. package/src/widgets/multichat-panel.tsx +62 -11
  59. 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.disposeLoadedModel('test-chat');
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
  });
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  import { IAttachment } from '../types';
3
2
  export type AttachmentsProps = {
4
3
  attachments: IAttachment[];
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  import { IUser } from '../types';
3
2
  /**
4
3
  * The avatar props.
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  import { IThemeManager } from '@jupyterlab/apputils';
3
2
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
4
3
  import { ITranslator } from '@jupyterlab/translation';
@@ -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
- ...props,
46
- inputToolbarRegistry
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: {
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  import { IChatModel } from '../../model';
3
2
  export type CodeToolbarProps = {
4
3
  /**
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  type CopyButtonProps = {
3
2
  value: string;
4
3
  className?: string;
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  import { InputToolbarRegistry } from '../toolbar-registry';
3
2
  /**
4
3
  * The attach button.
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  import { InputToolbarRegistry } from '../toolbar-registry';
3
2
  /**
4
3
  * The cancel button.
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  import { InputToolbarRegistry } from '../toolbar-registry';
3
2
  /**
4
3
  * The save edit button.
@@ -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 (chatCommandRegistry === null || chatCommandRegistry === void 0 ? void 0 : chatCommandRegistry.onSubmit(model));
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,
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  import { InputToolbarRegistry } from '../toolbar-registry';
3
2
  /**
4
3
  * The send button, with optional 'include selection' menu.
@@ -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
- // Run all command providers
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.send(body);
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: {
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  import { InputToolbarRegistry } from '../toolbar-registry';
3
2
  /**
4
3
  * The stop button.
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  import { SxProps, Theme } from '@mui/material';
3
2
  import { IInputModel } from '../../input-model';
4
3
  export declare function ChatInput(props: ChatInput.IProps): JSX.Element;
@@ -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
- // Run all command providers
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,3 @@
1
- /// <reference types="react" />
2
1
  /**
3
2
  * Component that renders chat body placeholder using the factory from context.
4
3
  * Renders nothing if no factory is provided.
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  import { IMessageContent } from '../../types';
3
2
  /**
4
3
  * The chat footer component properties.
@@ -1,4 +1,4 @@
1
- /// <reference types="react" />
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 ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element;
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 ChatMessageHeader(props) {
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 const ChatMessage: React.ForwardRefExoticComponent<ChatMessageProps & React.RefAttributes<HTMLDivElement>>;
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, { forwardRef, useEffect, useState } from '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 const ChatMessage = forwardRef((props, ref) => {
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
- return deleted ? (React.createElement("div", { ref: ref, "data-index": props.index })) : (React.createElement("div", { ref: ref, "data-index": props.index, className: MESSAGE_CONTAINER_CLASS },
115
- 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: props.renderedPromise })),
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);
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  export declare const MESSAGE_CLASS = "jp-chat-message";
3
2
  /**
4
3
  * The messages list component.
@@ -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 message as been rendered once.
130
+ // Used on first rendering, to ensure all the messages have been rendered once.
134
131
  if (!allRendered) {
135
- Promise.all(renderedPromise.current.map(p => p.promise)).then(() => {
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 , because more message are in the viewport
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
- listRef.current.forEach(item => {
174
- if (item) {
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
- listRef.current.forEach(item => {
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, renderedPromise: renderedPromise.current[i], ref: el => (listRef.current[i] = el) }),
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 })));
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  import { IMessage } from '../../types';
3
2
  /**
4
3
  * The preamble component properties.
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  /**
3
2
  * The toolbar attached to a message.
4
3
  */
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  /**
3
2
  * The component props.
4
3
  */
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  import { IconButtonProps } from '@mui/material';
3
2
  import { TooltippedButtonProps } from './tooltipped-button';
4
3
  /**
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  import { SxProps, Theme } from '@mui/material';
3
2
  import { IChatModel } from '../model';
4
3
  /**
package/lib/context.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  import { TranslationBundle } from '@jupyterlab/translation';
3
2
  import { Chat } from './components';
4
3
  export declare const TRANSLATION_DOMAIN = "jupyter-chat";
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
  }