Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/jupyter-chat-example/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
ActiveCellManager,
AttachmentOpenerRegistry,
IAttachment,
IChatMessage,
IChatContext,
IConfig,
IMessageContent,
INewMessage,
MultiChatPanel,
SelectionWatcher,
Expand Down Expand Up @@ -58,7 +58,7 @@ class MyChatModel extends AbstractChatModel {
sendMessage(
newMessage: INewMessage
): Promise<boolean | void> | boolean | void {
const message: IChatMessage = {
const message: IMessageContent = {
body: newMessage.body,
id: newMessage.id ?? UUID.uuid4(),
type: 'msg',
Expand Down
14 changes: 7 additions & 7 deletions packages/jupyter-chat/src/__tests__/model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

import { AbstractChatModel, IChatContext, IChatModel } from '../model';
import { IChatMessage, INewMessage } from '../types';
import { IMessage, IMessageContent, INewMessage } from '../types';
import { MockChatModel, MockChatContext } from './mocks';

describe('test chat model', () => {
Expand All @@ -27,7 +27,7 @@ describe('test chat model', () => {

describe('incoming message', () => {
class TestChat extends AbstractChatModel implements IChatModel {
protected formatChatMessage(message: IChatMessage): IChatMessage {
protected formatChatMessage(message: IMessageContent): IMessageContent {
message.body = 'formatted msg';
return message;
}
Expand All @@ -43,14 +43,14 @@ describe('test chat model', () => {
}

let model: IChatModel;
let messages: IChatMessage[];
let messages: IMessage[];
const msg = {
type: 'msg',
id: 'message1',
time: Date.now() / 1000,
body: 'message test',
sender: { username: 'user' }
} as IChatMessage;
} as IMessageContent;

beforeEach(() => {
messages = [];
Expand All @@ -64,7 +64,7 @@ describe('test chat model', () => {
});
model.messageAdded(msg);
expect(messages).toHaveLength(1);
expect(messages[0]).toBe(msg);
expect(messages[0].content).toBe(msg);
});

it('should format message', () => {
Expand All @@ -75,8 +75,8 @@ describe('test chat model', () => {
});
model.messageAdded({ ...msg });
expect(messages).toHaveLength(1);
expect(messages[0]).not.toBe(msg);
expect((messages[0] as IChatMessage).body).toBe('formatted msg');
expect(messages[0].content).not.toBe(msg);
expect(messages[0].body).toBe('formatted msg');
});
});

Expand Down
19 changes: 16 additions & 3 deletions packages/jupyter-chat/src/components/messages/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Box, Typography } from '@mui/material';
import React, { useEffect, useState } from 'react';

import { Avatar } from '../avatar';
import { IChatMessage } from '../../types';
import { IMessageContent, IMessage } from '../../types';

const MESSAGE_HEADER_CLASS = 'jp-chat-message-header';
const MESSAGE_TIME_CLASS = 'jp-chat-message-time';
Expand All @@ -19,7 +19,7 @@ type ChatMessageHeaderProps = {
/**
* The chat message.
*/
message: IChatMessage;
message: IMessage;
/**
* Whether this message is from the current user.
*/
Expand All @@ -35,7 +35,9 @@ export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
return <></>;
}
const [datetime, setDatetime] = useState<Record<number, string>>({});
const message = props.message;
const [message, setMessage] = useState<IMessageContent>(
props.message.content
);
const sender = message.sender;
/**
* Effect: update cached datetime strings upon receiving a new message.
Expand Down Expand Up @@ -74,6 +76,17 @@ export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
}
});

// Listen for changes in the current message.
useEffect(() => {
function messageChanged() {
setMessage(props.message.content);
}
props.message.changed.connect(messageChanged);
return () => {
props.message.changed.disconnect(messageChanged);
};
}, [props.message]);

const avatar = message.stacked ? null : Avatar({ user: sender });

const name =
Expand Down
27 changes: 24 additions & 3 deletions packages/jupyter-chat/src/components/messages/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { BaseMessageProps } from './messages';
import { AttachmentPreviewList } from '../attachments';
import { ChatInput } from '../input';
import { IInputModel, InputModel } from '../../input-model';
import { IChatMessage } from '../../types';
import { IMessageContent, IMessage } from '../../types';
import { replaceSpanToMention } from '../../utils';

/**
Expand All @@ -21,7 +21,7 @@ type ChatMessageProps = BaseMessageProps & {
/**
* The message to display.
*/
message: IChatMessage;
message: IMessage;
/**
* The index of the message in the list.
*/
Expand All @@ -37,7 +37,10 @@ type ChatMessageProps = BaseMessageProps & {
*/
export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
(props, ref): JSX.Element => {
const { message, model, rmRegistry } = props;
const { model, rmRegistry } = props;
const [message, setMessage] = useState<IMessageContent>(
props.message.content
);
const [edit, setEdit] = useState<boolean>(false);
const [deleted, setDeleted] = useState<boolean>(false);
const [canEdit, setCanEdit] = useState<boolean>(false);
Expand All @@ -62,6 +65,24 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
}
}, [model, message]);

// Listen for changes in the current message.
useEffect(() => {
function messageChanged() {
setMessage(props.message.content);
}

props.message.changed.connect(messageChanged);

// Initialize the message when the message is re-rendered.
// FIX ? This seems to be required for outofband change, to get the new value,
// even if when an outofband change occurs, all the messages are deleted and
// recreated.
setMessage(props.message.content);
return () => {
props.message.changed.disconnect(messageChanged);
};
}, [props.message]);

// Create an input model only if the message is edited.
const startEdition = (): void => {
if (!canEdit) {
Expand Down
15 changes: 10 additions & 5 deletions packages/jupyter-chat/src/components/messages/messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import { Navigation } from './navigation';
import { WelcomeMessage } from './welcome';
import { IInputToolbarRegistry } from '../input';
import { ScrollContainer } from '../scroll-container';
import { IChatCommandRegistry, IMessageFooterRegistry } from '../../registers';
import { Message } from '../../message';
import { IChatModel } from '../../model';
import { ChatArea, IChatMessage } from '../../types';
import { IChatCommandRegistry, IMessageFooterRegistry } from '../../registers';
import { ChatArea, IMessage } from '../../types';

export const MESSAGE_CLASS = 'jp-chat-message';
const MESSAGES_BOX_CLASS = 'jp-chat-messages-container';
Expand Down Expand Up @@ -63,7 +64,7 @@ export type BaseMessageProps = {
*/
export function ChatMessages(props: BaseMessageProps): JSX.Element {
const { model } = props;
const [messages, setMessages] = useState<IChatMessage[]>(model.messages);
const [messages, setMessages] = useState<IMessage[]>(model.messages);
const refMsgBox = useRef<HTMLDivElement>(null);
const [allRendered, setAllRendered] = useState<boolean>(false);

Expand All @@ -81,7 +82,11 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
}
model
.getHistory()
.then(history => setMessages(history.messages))
.then(history =>
setMessages(
history.messages.map(message => new Message({ ...message }))
)
)
.catch(e => console.error(e));
}

Expand Down Expand Up @@ -198,7 +203,7 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
return (
// extra div needed to ensure each bubble is on a new line
<Box
key={i}
key={message.id}
sx={{
...(isCurrentUser && {
marginLeft: props.area === 'main' ? '25%' : '10%',
Expand Down
83 changes: 83 additions & 0 deletions packages/jupyter-chat/src/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright (c) Jupyter Development Team.
* Distributed under the terms of the Modified BSD License.
*/

import { ISignal, Signal } from '@lumino/signaling';
import { IAttachment, IMessageContent, IMessage, IUser } from './types';

/**
* The message object.
*/
export class Message implements IMessage {
/**
* The constructor of the message.
*
* @param content: the content of the message.
*/
constructor(content: IMessageContent) {
this._content = content;
}

/**
* The message content.
*/
get content(): IMessageContent {
return this._content;
}

/**
* Getters for each attribute individually.
*/
get type(): string {
return this._content.type;
}
get body(): string {
return this._content.body;
}
get id(): string {
return this._content.id;
}
get time(): number {
return this._content.time;
}
get sender(): IUser {
return this._content.sender;
}
get attachments(): IAttachment[] | undefined {
return this._content.attachments;
}
get mentions(): IUser[] | undefined {
return this._content.mentions;
}
get raw_time(): boolean | undefined {
return this._content.raw_time;
}
get deleted(): boolean | undefined {
return this._content.deleted;
}
get edited(): boolean | undefined {
return this._content.edited;
}
get stacked(): boolean | undefined {
return this._content.stacked;
}

/**
* A signal emitting when the message has been updated.
*/
get changed(): ISignal<IMessage, void> {
return this._changed;
}

/**
* Update one or several fields of the message.
*/
update(updated: Partial<IMessageContent>) {
this._content = { ...this._content, ...updated };
this._changed.emit();
}

private _content: IMessageContent;
private _changed = new Signal<IMessage, void>(this);
}
Loading
Loading