Skip to content

Commit 0d21e56

Browse files
authored
Fix message datetime (#29)
* 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
1 parent 4875e78 commit 0d21e56

File tree

3 files changed

+90
-73
lines changed

3 files changed

+90
-73
lines changed

packages/jupyter-chat/src/components/chat-input.tsx

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Distributed under the terms of the Modified BSD License.
44
*/
55

6-
import React from 'react';
6+
import React, { useState } from 'react';
77

88
import {
99
Box,
@@ -21,18 +21,36 @@ const SEND_BUTTON_CLASS = 'jp-chat-send-button';
2121
const CANCEL_BUTTON_CLASS = 'jp-chat-cancel-button';
2222

2323
export function ChatInput(props: ChatInput.IProps): JSX.Element {
24+
const [input, setInput] = useState(props.value || '');
25+
2426
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
2527
if (
2628
event.key === 'Enter' &&
2729
((props.sendWithShiftEnter && event.shiftKey) ||
2830
(!props.sendWithShiftEnter && !event.shiftKey))
2931
) {
30-
props.onSend();
32+
onSend();
3133
event.stopPropagation();
3234
event.preventDefault();
3335
}
3436
}
3537

38+
/**
39+
* Triggered when sending the message.
40+
*/
41+
function onSend() {
42+
setInput('');
43+
props.onSend(input);
44+
}
45+
46+
/**
47+
* Triggered when cancelling edition.
48+
*/
49+
function onCancel() {
50+
setInput(props.value || '');
51+
props.onCancel!();
52+
}
53+
3654
// Set the helper text based on whether Shift+Enter is used for sending.
3755
const helperText = props.sendWithShiftEnter ? (
3856
<span>
@@ -48,8 +66,8 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
4866
<Box sx={props.sx} className={clsx(INPUT_BOX_CLASS)}>
4967
<Box sx={{ display: 'flex' }}>
5068
<TextField
51-
value={props.value}
52-
onChange={e => props.onChange(e.target.value)}
69+
value={input}
70+
onChange={e => setInput(e.target.value)}
5371
fullWidth
5472
variant="outlined"
5573
multiline
@@ -62,8 +80,8 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
6280
<IconButton
6381
size="small"
6482
color="primary"
65-
onClick={props.onCancel}
66-
disabled={!props.value.trim().length}
83+
onClick={onCancel}
84+
disabled={!input.trim().length}
6785
title={'Cancel edition'}
6886
className={clsx(CANCEL_BUTTON_CLASS)}
6987
>
@@ -73,8 +91,8 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
7391
<IconButton
7492
size="small"
7593
color="primary"
76-
onClick={props.onSend}
77-
disabled={!props.value.trim().length}
94+
onClick={onSend}
95+
disabled={!input.trim().length}
7896
title={`Send message ${props.sendWithShiftEnter ? '(SHIFT+ENTER)' : '(ENTER)'}`}
7997
className={clsx(SEND_BUTTON_CLASS)}
8098
>
@@ -86,7 +104,7 @@ export function ChatInput(props: ChatInput.IProps): JSX.Element {
86104
FormHelperTextProps={{
87105
sx: { marginLeft: 'auto', marginRight: 0 }
88106
}}
89-
helperText={props.value.length > 2 ? helperText : ' '}
107+
helperText={input.length > 2 ? helperText : ' '}
90108
/>
91109
</Box>
92110
</Box>
@@ -101,11 +119,25 @@ export namespace ChatInput {
101119
* The properties of the react element.
102120
*/
103121
export interface IProps {
104-
value: string;
105-
onChange: (newValue: string) => unknown;
106-
onSend: () => unknown;
107-
sendWithShiftEnter: boolean;
122+
/**
123+
* The initial value of the input (default to '')
124+
*/
125+
value?: string;
126+
/**
127+
* The function to be called to send the message.
128+
*/
129+
onSend: (input: string) => unknown;
130+
/**
131+
* The function to be called to cancel editing.
132+
*/
108133
onCancel?: () => unknown;
134+
/**
135+
* Whether using shift+enter to send the message.
136+
*/
137+
sendWithShiftEnter: boolean;
138+
/**
139+
* Custom mui/material styles.
140+
*/
109141
sx?: SxProps<Theme>;
110142
}
111143
}

packages/jupyter-chat/src/components/chat-messages.tsx

Lines changed: 44 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,57 @@ type ChatMessagesProps = BaseMessageProps & {
3333
};
3434

3535
export type ChatMessageHeaderProps = IUser & {
36-
timestamp: string;
36+
timestamp: number;
3737
rawTime?: boolean;
3838
deleted?: boolean;
3939
edited?: boolean;
4040
sx?: SxProps<Theme>;
4141
};
4242

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

50+
/**
51+
* Effect: update cached datetime strings upon receiving a new message.
52+
*/
53+
useEffect(() => {
54+
if (!datetime[props.timestamp]) {
55+
const newDatetime: Record<number, string> = {};
56+
let datetime: string;
57+
const currentDate = new Date();
58+
const sameDay = (date: Date) =>
59+
date.getFullYear() === currentDate.getFullYear() &&
60+
date.getMonth() === currentDate.getMonth() &&
61+
date.getDate() === currentDate.getDate();
62+
63+
const msgDate = new Date(props.timestamp * 1000); // Convert message time to milliseconds
64+
65+
// Display only the time if the day of the message is the current one.
66+
if (sameDay(msgDate)) {
67+
// Use the browser's default locale
68+
datetime = msgDate.toLocaleTimeString([], {
69+
hour: 'numeric',
70+
minute: '2-digit'
71+
});
72+
} else {
73+
// Use the browser's default locale
74+
datetime = msgDate.toLocaleString([], {
75+
day: 'numeric',
76+
month: 'numeric',
77+
year: 'numeric',
78+
hour: 'numeric',
79+
minute: '2-digit'
80+
});
81+
}
82+
newDatetime[props.timestamp] = datetime;
83+
setDatetime(newDatetime);
84+
}
85+
});
86+
4987
const bgcolor = props.color;
5088
const avatar = props.avatar_url ? (
5189
<Avatar
@@ -125,7 +163,7 @@ export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
125163
}}
126164
title={props.rawTime ? 'Unverified time' : ''}
127165
>
128-
{`${props.timestamp}${props.rawTime ? '*' : ''}`}
166+
{`${datetime[props.timestamp]}${props.rawTime ? '*' : ''}`}
129167
</Typography>
130168
</Box>
131169
</Box>
@@ -136,51 +174,6 @@ export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element {
136174
* The messages list UI.
137175
*/
138176
export function ChatMessages(props: ChatMessagesProps): JSX.Element {
139-
const [timestamps, setTimestamps] = useState<Record<string, string>>({});
140-
141-
/**
142-
* Effect: update cached timestamp strings upon receiving a new message.
143-
*/
144-
useEffect(() => {
145-
const newTimestamps: Record<string, string> = { ...timestamps };
146-
let timestampAdded = false;
147-
148-
const currentDate = new Date();
149-
const sameDay = (date: Date) =>
150-
date.getFullYear() === currentDate.getFullYear() &&
151-
date.getMonth() === currentDate.getMonth() &&
152-
date.getDate() === currentDate.getDate();
153-
154-
for (const message of props.messages) {
155-
if (!(message.id in newTimestamps)) {
156-
const date = new Date(message.time * 1000); // Convert message time to milliseconds
157-
158-
// Display only the time if the day of the message is the current one.
159-
if (sameDay(date)) {
160-
// Use the browser's default locale
161-
newTimestamps[message.id] = date.toLocaleTimeString([], {
162-
hour: 'numeric',
163-
minute: '2-digit'
164-
});
165-
} else {
166-
// Use the browser's default locale
167-
newTimestamps[message.id] = date.toLocaleString([], {
168-
day: 'numeric',
169-
month: 'numeric',
170-
year: 'numeric',
171-
hour: 'numeric',
172-
minute: '2-digit'
173-
});
174-
}
175-
176-
timestampAdded = true;
177-
}
178-
}
179-
if (timestampAdded) {
180-
setTimestamps(newTimestamps);
181-
}
182-
}, [props.messages]);
183-
184177
return (
185178
<Box
186179
sx={{
@@ -206,7 +199,7 @@ export function ChatMessages(props: ChatMessagesProps): JSX.Element {
206199
>
207200
<ChatMessageHeader
208201
{...sender}
209-
timestamp={timestamps[message.id]}
202+
timestamp={message.time}
210203
rawTime={message.raw_time}
211204
deleted={message.deleted}
212205
edited={message.edited}
@@ -241,14 +234,12 @@ export function ChatMessage(props: ChatMessageProps): JSX.Element {
241234
}
242235
}
243236
const [edit, setEdit] = useState<boolean>(false);
244-
const [input, setInput] = useState(message.body);
245237

246238
const cancelEdition = (): void => {
247-
setInput(message.body);
248239
setEdit(false);
249240
};
250241

251-
const updateMessage = (id: string): void => {
242+
const updateMessage = (id: string, input: string): void => {
252243
if (!canEdit) {
253244
return;
254245
}
@@ -274,9 +265,8 @@ export function ChatMessage(props: ChatMessageProps): JSX.Element {
274265
<div>
275266
{edit && canEdit ? (
276267
<ChatInput
277-
value={input}
278-
onChange={setInput}
279-
onSend={() => updateMessage(message.id)}
268+
value={message.body}
269+
onSend={(input: string) => updateMessage(message.id, input)}
280270
onCancel={() => cancelEdition()}
281271
sendWithShiftEnter={model.config.sendWithShiftEnter ?? false}
282272
/>

packages/jupyter-chat/src/components/chat.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ function ChatBody({
2828
rmRegistry: renderMimeRegistry
2929
}: ChatBodyProps): JSX.Element {
3030
const [messages, setMessages] = useState<IChatMessage[]>([]);
31-
const [input, setInput] = useState('');
3231

3332
/**
3433
* Effect: fetch history and config on initial render
@@ -99,9 +98,7 @@ function ChatBody({
9998

10099
// no need to append to messageGroups imperatively here. all of that is
101100
// handled by the listeners registered in the effect hooks above.
102-
const onSend = async () => {
103-
setInput('');
104-
101+
const onSend = async (input: string) => {
105102
// send message to backend
106103
model.addMessage({ body: input });
107104
};
@@ -116,8 +113,6 @@ function ChatBody({
116113
/>
117114
</ScrollContainer>
118115
<ChatInput
119-
value={input}
120-
onChange={setInput}
121116
onSend={onSend}
122117
sx={{
123118
paddingLeft: 4,

0 commit comments

Comments
 (0)