Skip to content

Commit

Permalink
Fix message datetime (#29)
Browse files Browse the repository at this point in the history
* Avoid reloading all the messages when the input value changes, by moving the input state to the input itself instead of the chat

* Save the string datetime in the header instead of the message, and recalculate it if the timestamp change
  • Loading branch information
brichet authored May 14, 2024
1 parent 4875e78 commit 0d21e56
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 73 deletions.
58 changes: 45 additions & 13 deletions packages/jupyter-chat/src/components/chat-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Distributed under the terms of the Modified BSD License.
*/

import React from 'react';
import React, { useState } from 'react';

import {
Box,
Expand All @@ -21,18 +21,36 @@ const SEND_BUTTON_CLASS = 'jp-chat-send-button';
const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button';

export function ChatInput(props: ChatInput.IProps): JSX.Element {
const [input, setInput] = useState(props.value || '');

function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
if (
event.key === 'Enter' &&
((props.sendWithShiftEnter && event.shiftKey) ||
(!props.sendWithShiftEnter && !event.shiftKey))
) {
props.onSend();
onSend();
event.stopPropagation();
event.preventDefault();
}
}

/**
* Triggered when sending the message.
*/
function onSend() {
setInput('');
props.onSend(input);
}

/**
* Triggered when cancelling edition.
*/
function onCancel() {
setInput(props.value || '');
props.onCancel!();
}

// Set the helper text based on whether Shift+Enter is used for sending.
const helperText = props.sendWithShiftEnter ? (
<span>
Expand All @@ -48,8 +66,8 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
<Box sx={props.sx} className={clsx(INPUT_BOX_CLASS)}>
<Box sx={{ display: 'flex' }}>
<TextField
value={props.value}
onChange={e => props.onChange(e.target.value)}
value={input}
onChange={e => setInput(e.target.value)}
fullWidth
variant="outlined"
multiline
Expand All @@ -62,8 +80,8 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
<IconButton
size="small"
color="primary"
onClick={props.onCancel}
disabled={!props.value.trim().length}
onClick={onCancel}
disabled={!input.trim().length}
title={'Cancel edition'}
className={clsx(CANCEL_BUTTON_CLASS)}
>
Expand All @@ -73,8 +91,8 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
<IconButton
size="small"
color="primary"
onClick={props.onSend}
disabled={!props.value.trim().length}
onClick={onSend}
disabled={!input.trim().length}
title={`Send message ${props.sendWithShiftEnter ? '(SHIFT+ENTER)' : '(ENTER)'}`}
className={clsx(SEND_BUTTON_CLASS)}
>
Expand All @@ -86,7 +104,7 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
FormHelperTextProps={{
sx: { marginLeft: 'auto', marginRight: 0 }
}}
helperText={props.value.length > 2 ? helperText : ' '}
helperText={input.length > 2 ? helperText : ' '}
/>
</Box>
</Box>
Expand All @@ -101,11 +119,25 @@ export namespace ChatInput {
* The properties of the react element.
*/
export interface IProps {
value: string;
onChange: (newValue: string) => unknown;
onSend: () => unknown;
sendWithShiftEnter: boolean;
/**
* The initial value of the input (default to '')
*/
value?: string;
/**
* The function to be called to send the message.
*/
onSend: (input: string) => unknown;
/**
* The function to be called to cancel editing.
*/
onCancel?: () => unknown;
/**
* Whether using shift+enter to send the message.
*/
sendWithShiftEnter: boolean;
/**
* Custom mui/material styles.
*/
sx?: SxProps<Theme>;
}
}
98 changes: 44 additions & 54 deletions packages/jupyter-chat/src/components/chat-messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,57 @@ type ChatMessagesProps = BaseMessageProps & {
};

export type ChatMessageHeaderProps = IUser & {
timestamp: string;
timestamp: number;
rawTime?: boolean;
deleted?: boolean;
edited?: boolean;
sx?: SxProps<Theme>;
};

export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
const [datetime, setDatetime] = useState<Record<number, string>>({});
const sharedStyles: SxProps<Theme> = {
height: '24px',
width: '24px'
};

/**
* Effect: update cached datetime strings upon receiving a new message.
*/
useEffect(() => {
if (!datetime[props.timestamp]) {
const newDatetime: Record<number, string> = {};
let datetime: string;
const currentDate = new Date();
const sameDay = (date: Date) =>
date.getFullYear() === currentDate.getFullYear() &&
date.getMonth() === currentDate.getMonth() &&
date.getDate() === currentDate.getDate();

const msgDate = new Date(props.timestamp * 1000); // Convert message time to milliseconds

// Display only the time if the day of the message is the current one.
if (sameDay(msgDate)) {
// Use the browser's default locale
datetime = msgDate.toLocaleTimeString([], {
hour: 'numeric',
minute: '2-digit'
});
} else {
// Use the browser's default locale
datetime = msgDate.toLocaleString([], {
day: 'numeric',
month: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit'
});
}
newDatetime[props.timestamp] = datetime;
setDatetime(newDatetime);
}
});

const bgcolor = props.color;
const avatar = props.avatar_url ? (
<Avatar
Expand Down Expand Up @@ -125,7 +163,7 @@ export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
}}
title={props.rawTime ? 'Unverified time' : ''}
>
{`${props.timestamp}${props.rawTime ? '*' : ''}`}
{`${datetime[props.timestamp]}${props.rawTime ? '*' : ''}`}
</Typography>
</Box>
</Box>
Expand All @@ -136,51 +174,6 @@ export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
* The messages list UI.
*/
export function ChatMessages(props: ChatMessagesProps): JSX.Element {
const [timestamps, setTimestamps] = useState<Record<string, string>>({});

/**
* Effect: update cached timestamp strings upon receiving a new message.
*/
useEffect(() => {
const newTimestamps: Record<string, string> = { ...timestamps };
let timestampAdded = false;

const currentDate = new Date();
const sameDay = (date: Date) =>
date.getFullYear() === currentDate.getFullYear() &&
date.getMonth() === currentDate.getMonth() &&
date.getDate() === currentDate.getDate();

for (const message of props.messages) {
if (!(message.id in newTimestamps)) {
const date = new Date(message.time * 1000); // Convert message time to milliseconds

// Display only the time if the day of the message is the current one.
if (sameDay(date)) {
// Use the browser's default locale
newTimestamps[message.id] = date.toLocaleTimeString([], {
hour: 'numeric',
minute: '2-digit'
});
} else {
// Use the browser's default locale
newTimestamps[message.id] = date.toLocaleString([], {
day: 'numeric',
month: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit'
});
}

timestampAdded = true;
}
}
if (timestampAdded) {
setTimestamps(newTimestamps);
}
}, [props.messages]);

return (
<Box
sx={{
Expand All @@ -206,7 +199,7 @@ export function ChatMessages(props: ChatMessagesProps): JSX.Element {
>
<ChatMessageHeader
{...sender}
timestamp={timestamps[message.id]}
timestamp={message.time}
rawTime={message.raw_time}
deleted={message.deleted}
edited={message.edited}
Expand Down Expand Up @@ -241,14 +234,12 @@ export function ChatMessage(props: ChatMessageProps): JSX.Element {
}
}
const [edit, setEdit] = useState<boolean>(false);
const [input, setInput] = useState(message.body);

const cancelEdition = (): void => {
setInput(message.body);
setEdit(false);
};

const updateMessage = (id: string): void => {
const updateMessage = (id: string, input: string): void => {
if (!canEdit) {
return;
}
Expand All @@ -274,9 +265,8 @@ export function ChatMessage(props: ChatMessageProps): JSX.Element {
<div>
{edit && canEdit ? (
<ChatInput
value={input}
onChange={setInput}
onSend={() => updateMessage(message.id)}
value={message.body}
onSend={(input: string) => updateMessage(message.id, input)}
onCancel={() => cancelEdition()}
sendWithShiftEnter={model.config.sendWithShiftEnter ?? false}
/>
Expand Down
7 changes: 1 addition & 6 deletions packages/jupyter-chat/src/components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ function ChatBody({
rmRegistry: renderMimeRegistry
}: ChatBodyProps): JSX.Element {
const [messages, setMessages] = useState<IChatMessage[]>([]);
const [input, setInput] = useState('');

/**
* Effect: fetch history and config on initial render
Expand Down Expand Up @@ -99,9 +98,7 @@ function ChatBody({

// no need to append to messageGroups imperatively here. all of that is
// handled by the listeners registered in the effect hooks above.
const onSend = async () => {
setInput('');

const onSend = async (input: string) => {
// send message to backend
model.addMessage({ body: input });
};
Expand All @@ -116,8 +113,6 @@ function ChatBody({
/>
</ScrollContainer>
<ChatInput
value={input}
onChange={setInput}
onSend={onSend}
sx={{
paddingLeft: 4,
Expand Down

0 comments on commit 0d21e56

Please sign in to comment.