Skip to content

Commit 5633614

Browse files
authored
feat: add message edited timestamp (#2304)
### 🎯 Goal πŸš‚ GetStream/stream-chat-css#275 πŸš‚ GetStream/stream-chat-js#1248 Adds a new collapsible section in message metadata that shows the last time message text was updated. Also, clearly labels edited messages. ### 🎨 UI Changes See GetStream/stream-chat-css#275 ### To-Do - [x] Bump LLC - [x] Bump styles - [x] Translations
1 parent 024ba6c commit 5633614

26 files changed

+261
-56
lines changed

β€Ždocusaurus/docs/React/components/contexts/component-context.mdx

+13-3
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ Custom UI component to display attachment in an individual message.
3232

3333
Custom UI component to display a attachment previews in `MessageInput`.
3434

35-
| Type | Default |
36-
| --------- | ----------------------------------------------------------------------------------- |
35+
| Type | Default |
36+
| --------- | ---------------------------------------------------------------------------------------------- |
3737
| component | <GHComponentLink text='AttachmentPreviewList' path='/MessageInput/AttachmentPreviewList.tsx'/> |
3838

3939
### AutocompleteSuggestionHeader
@@ -256,12 +256,14 @@ Custom UI component to display system messages.
256256

257257
### MessageTimestamp
258258

259-
Custom UI component to display a timestamp on a message.
259+
Custom UI component to display a timestamp on a message. This does not include a timestamp for edited messages.
260260

261261
| Type | Default |
262262
| --------- | ------------------------------------------------------------------------------- |
263263
| component | <GHComponentLink text='MessageTimestamp' path='/Message/MessageTimestamp.tsx'/> |
264264

265+
See also [`Timestamp`](#timestamp).
266+
265267
### MessageBouncePrompt
266268

267269
Custom UI component for the content of the modal dialog for messages that got bounced by the moderation rules.
@@ -358,6 +360,14 @@ Custom UI component to display the start of a threaded `MessageList`.
358360
| --------- | ---------------------------------------------------------------------- |
359361
| component | <GHComponentLink text='DefaultThreadStart' path='/Thread/Thread.tsx'/> |
360362

363+
### Timestamp
364+
365+
Custom UI component to display a date used in timestamps. It's used internally by the default `MessageTimestamp`, and to display a timestamp for edited messages.
366+
367+
| Type | Default |
368+
| --------- | ----------------------------------------------------------------- |
369+
| component | <GHComponentLink text='Timestamp' path='/Message/Timestamp.tsx'/> |
370+
361371
### TriggerProvider
362372

363373
Optional context provider that lets you override the default autocomplete triggers.

β€Ždocusaurus/docs/React/components/core-components/channel.mdx

+18-8
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,8 @@ Custom UI component to display a message attachment.
138138

139139
Custom UI component to display an attachment previews in `MessageInput`.
140140

141-
| Type | Default |
142-
| --------- | ----------------------------------------------------------------------------------- |
141+
| Type | Default |
142+
| --------- | ---------------------------------------------------------------------------------------------- |
143143
| component | <GHComponentLink text='AttachmentPreviewList' path='/MessageInput/AttachmentPreviewList.tsx'/> |
144144

145145
### AutocompleteSuggestionHeader
@@ -384,8 +384,8 @@ Custom UI component to render at the top of the `MessageList`.
384384

385385
A custom function to provide size configuration for image attachments
386386

387-
| Type |
388-
| ---------------------------------------------------------------- |
387+
| Type |
388+
| ----------------------------------------------------------------- |
389389
| `(a: Attachment, e: HTMLElement) => ImageAttachmentConfiguration` |
390390

391391
### initializeOnMount
@@ -445,7 +445,7 @@ Configuration parameter to mark the active channel as read when mounted (opened)
445445

446446
| Type | Default |
447447
| ------- | ------- |
448-
| boolean | true |
448+
| boolean | true |
449449

450450
### Input
451451

@@ -459,8 +459,8 @@ Custom UI component handling how the message input is rendered.
459459

460460
Custom component to render link previews in `MessageInput`.
461461

462-
| Type | Default |
463-
| --------- | ----------------------------------------------------------------------------------- |
462+
| Type | Default |
463+
| --------- | ---------------------------------------------------------------------------------- |
464464
| component | <GHComponentLink text='LinkPreviewList' path='/MessageInput/LinkPreviewList.tsx'/> |
465465

466466
### LoadingErrorIndicator
@@ -553,12 +553,14 @@ Custom UI component to display system messages.
553553

554554
### MessageTimestamp
555555

556-
Custom UI component to display a timestamp on a message.
556+
Custom UI component to display a timestamp on a message. This does not include a timestamp for edited messages.
557557

558558
| Type | Default |
559559
| --------- | ------------------------------------------------------------------------------- |
560560
| component | <GHComponentLink text='MessageTimestamp' path='/Message/MessageTimestamp.tsx'/> |
561561

562+
See also [`Timestamp`](#timestamp).
563+
562564
### MessageBouncePrompt
563565

564566
Custom UI component for the content of the modal dialog for messages that got bounced by the moderation rules.
@@ -703,6 +705,14 @@ Custom UI component to display the start of a threaded `MessageList`.
703705
| --------- | ---------------------------------------------------------------------- |
704706
| component | <GHComponentLink text='DefaultThreadStart' path='/Thread/Thread.tsx'/> |
705707

708+
### Timestamp
709+
710+
Custom UI component to display a date used in timestamps. It's used internally by the default `MessageTimestamp`, and to display a timestamp for edited messages.
711+
712+
| Type | Default |
713+
| --------- | ----------------------------------------------------------------- |
714+
| component | <GHComponentLink text='Timestamp' path='/Message/Timestamp.tsx'/> |
715+
706716
### TriggerProvider
707717

708718
Optional context provider that lets you override the default autocomplete triggers.

β€Ždocusaurus/docs/React/components/message-components/ui-components.mdx

+44-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ The following UI components are available for use:
3636
- [`QuotedMessage`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/QuotedMessage.tsx) - shows a quoted
3737
message UI wrapper when the sent message quotes a previous message
3838

39+
- [`Timestamp`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/Timestmap.tsx) - formats and displays a date,
40+
used by `MessageTimestamp` and for edited message timestamps.
41+
3942
- [`MessageBouncePrompt`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageBounce/MessageBouncePrompt.tsx) -
4043
presents options to deal with a message that got bounced by the moderation rules.
4144

@@ -350,6 +353,8 @@ Theme string to be added to CSS class names.
350353

351354
## MessageTimestamp Props
352355

356+
This component has all of the same props as the underlying [`Timestamp`](#timestamp-props), except that instead of `timestamp` it uses `message.created_at` value from the `MessageContext`.
357+
353358
### calendar
354359

355360
If true, call the `Day.js` calendar function to get the date string to display.
@@ -418,7 +423,7 @@ The side of the message list to render MML components.
418423
`QuotedMessage` only consumes context and does not accept any optional props.
419424
:::
420425

421-
## MessageBouncePrompt
426+
## MessageBouncePrompt props
422427

423428
This component is rendered in a modal dialog for messages that got bounced by the moderation rules.
424429

@@ -460,3 +465,41 @@ The Message UI component will pass this callback to close the modal dialog `Mess
460465
| Type |
461466
| ----------------- |
462467
| ReactEventHandler |
468+
469+
## Timestamp props
470+
471+
### calendar
472+
473+
If true, call the `Day.js` calendar function to get the date string to display.
474+
475+
| Type | Default |
476+
| ------- | ------- |
477+
| boolean | false |
478+
479+
### customClass
480+
481+
If provided, adds a CSS class name to the component's outer `time` container.
482+
483+
```jsx
484+
<time className={customClass} />
485+
```
486+
487+
| Type |
488+
| ------ |
489+
| string |
490+
491+
### format
492+
493+
If provided, overrides the default timestamp format.
494+
495+
| Type | Default |
496+
| ------ | ------- |
497+
| string | 'h:mmA' |
498+
499+
### timestamp
500+
501+
Either an ISO string with a date, or a Date object with a date to display.
502+
503+
| Type |
504+
| -------------- |
505+
| Date \| string |

β€Ždocusaurus/docs/React/guides/theming/message-ui.mdx

+4-3
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ The custom Message UI component built below imports and uses the following UI co
4949
- [`SimpleReactionsList`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Reactions/SimpleReactionsList.tsx) - displays
5050
a minimal list of the reactions added to a message (alternate option to [`ReactionsList`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Reactions/ReactionsList.tsx)).
5151

52+
- [`Timestamp`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/Timestmap.tsx) - formats and displays a date,
53+
used by `MessageTimestamp` and for edited message timestamps.
54+
5255
### How it Fits Together
5356

5457
The sample code below assembles the above UI building blocks into a fully featured Message UI component. The UI components allow you to
@@ -105,9 +108,7 @@ export const CustomMessage = () => {
105108
<MessageTimestamp />
106109
</div>
107110
</div>
108-
{showDetailedReactions && canReact && (
109-
<ReactionSelector ref={reactionSelectorRef} />
110-
)}
111+
{showDetailedReactions && canReact && <ReactionSelector ref={reactionSelectorRef} />}
111112
<MessageText />
112113
<MessageStatus />
113114
{hasAttachments && <Attachment attachments={message.attachments} />}

β€Žsrc/components/Channel/Channel.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ type ChannelPropsForwardedToComponentContext<
183183
ThreadHeader?: ComponentContextValue<StreamChatGenerics>['ThreadHeader'];
184184
/** Custom UI component to display the start of a threaded `MessageList`, defaults to and accepts same props as: [DefaultThreadStart](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Thread/Thread.tsx) */
185185
ThreadStart?: ComponentContextValue<StreamChatGenerics>['ThreadStart'];
186+
/** Custom UI component to display a date used in timestamps. It's used internally by the default `MessageTimestamp`, and to display a timestamp for edited messages. */
187+
Timestamp?: ComponentContextValue<StreamChatGenerics>['Timestamp'];
186188
/** Optional context provider that lets you override the default autocomplete triggers, defaults to: [DefaultTriggerProvider](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/DefaultTriggerProvider.tsx) */
187189
TriggerProvider?: ComponentContextValue<StreamChatGenerics>['TriggerProvider'];
188190
/** Custom UI component for the typing indicator, defaults to and accepts same props as: [TypingIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/TypingIndicator/TypingIndicator.tsx) */
@@ -1192,6 +1194,7 @@ const ChannelInner = <
11921194
ThreadHead: props.ThreadHead,
11931195
ThreadHeader: props.ThreadHeader,
11941196
ThreadStart: props.ThreadStart,
1197+
Timestamp: props.Timestamp,
11951198
TriggerProvider: props.TriggerProvider,
11961199
TypingIndicator: props.TypingIndicator,
11971200
UnreadMessagesNotification: props.UnreadMessagesNotification,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from 'react';
2+
3+
import clsx from 'clsx';
4+
import { useComponentContext, useMessageContext, useTranslationContext } from '../../context';
5+
import { Timestamp as DefaultTimestamp } from './Timestamp';
6+
import { isMessageEdited } from './utils';
7+
8+
import type { DefaultStreamChatGenerics } from '../../types';
9+
import type { MessageTimestampProps } from './MessageTimestamp';
10+
11+
export type MessageEditedTimestampProps<
12+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
13+
> = MessageTimestampProps<StreamChatGenerics> & {
14+
open: boolean;
15+
};
16+
17+
export function MessageEditedTimestamp<
18+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
19+
>({
20+
message: propMessage,
21+
open,
22+
...timestampProps
23+
}: MessageEditedTimestampProps<StreamChatGenerics>) {
24+
const { t } = useTranslationContext('MessageEditedTimestamp');
25+
const { message: contextMessage } = useMessageContext<StreamChatGenerics>(
26+
'MessageEditedTimestamp',
27+
);
28+
const { Timestamp = DefaultTimestamp } = useComponentContext('MessageEditedTimestamp');
29+
const message = propMessage || contextMessage;
30+
31+
if (!isMessageEdited(message)) {
32+
return null;
33+
}
34+
35+
return (
36+
<div
37+
className={clsx(
38+
'str-chat__message-edited-timestamp',
39+
open
40+
? 'str-chat__message-edited-timestamp--open'
41+
: 'str-chat__message-edited-timestamp--collapsed',
42+
)}
43+
data-testid='message-edited-timestamp'
44+
>
45+
{t<string>('Edited')}{' '}
46+
<Timestamp timestamp={message.message_text_updated_at} {...timestampProps} />
47+
</div>
48+
);
49+
}

β€Žsrc/components/Message/MessageSimple.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { MessageTimestamp as DefaultMessageTimestamp } from './MessageTimestamp'
1212
import {
1313
areMessageUIPropsEqual,
1414
isMessageBounced,
15+
isMessageEdited,
1516
messageHasAttachments,
1617
messageHasReactions,
1718
} from './utils';
@@ -34,6 +35,8 @@ import { MessageContextValue, useMessageContext } from '../../context/MessageCon
3435
import type { MessageUIComponentProps } from './types';
3536

3637
import type { DefaultStreamChatGenerics } from '../../types/types';
38+
import { useTranslationContext } from '../../context';
39+
import { MessageEditedTimestamp } from './MessageEditedTimestamp';
3740

3841
type MessageSimpleWithContextProps<
3942
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
@@ -66,7 +69,9 @@ const MessageSimpleWithContext = <
6669
threadList,
6770
} = props;
6871

72+
const { t } = useTranslationContext('MessageSimple');
6973
const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false);
74+
const [isEditedTimestampOpen, setEditedTimestampOpen] = useState(false);
7075

7176
const {
7277
Attachment,
@@ -106,13 +111,16 @@ const MessageSimpleWithContext = <
106111
const showReplyCountButton = !threadList && !!message.reply_count;
107112
const allowRetry = message.status === 'failed' && message.errorStatusCode !== 403;
108113
const isBounced = isMessageBounced(message);
114+
const isEdited = isMessageEdited(message);
109115

110116
let handleClick: (() => void) | undefined = undefined;
111117

112118
if (allowRetry) {
113119
handleClick = () => handleRetry(message);
114120
} else if (isBounced) {
115121
handleClick = () => setIsBounceDialogOpen(true);
122+
} else if (isEdited) {
123+
handleClick = () => setEditedTimestampOpen((prev) => !prev);
116124
}
117125

118126
const rootClassName = clsx(
@@ -228,6 +236,10 @@ const MessageSimpleWithContext = <
228236
</span>
229237
)}
230238
<MessageTimestamp calendar customClass='str-chat__message-simple-timestamp' />
239+
{isEdited && (
240+
<span className='str-chat__mesage-simple-edited'>{t<string>('Edited')}</span>
241+
)}
242+
{isEdited && <MessageEditedTimestamp calendar open={isEditedTimestampOpen} />}
231243
</div>
232244
)}
233245
</div>

β€Žsrc/components/Message/MessageTimestamp.tsx

+9-36
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import React, { useMemo } from 'react';
2-
3-
import { useMessageContext } from '../../context/MessageContext';
4-
import { isDate, useTranslationContext } from '../../context/TranslationContext';
1+
import React from 'react';
52

63
import type { StreamMessage } from '../../context/ChannelStateContext';
7-
84
import type { DefaultStreamChatGenerics } from '../../types/types';
9-
import { getDateString } from '../../i18n/utils';
5+
6+
import { useMessageContext } from '../../context/MessageContext';
7+
import { Timestamp as DefaultTimestamp } from './Timestamp';
8+
import { useComponentContext } from '../../context';
109

1110
export const defaultTimestampFormat = 'h:mmA';
1211

@@ -28,37 +27,11 @@ const UnMemoizedMessageTimestamp = <
2827
>(
2928
props: MessageTimestampProps<StreamChatGenerics>,
3029
) => {
31-
const {
32-
calendar = false,
33-
customClass = '',
34-
format = defaultTimestampFormat,
35-
message: propMessage,
36-
} = props;
37-
38-
const { formatDate, message: contextMessage } = useMessageContext<StreamChatGenerics>(
39-
'MessageTimestamp',
40-
);
41-
const { tDateTimeParser } = useTranslationContext('MessageTimestamp');
42-
30+
const { message: propMessage, ...timestampProps } = props;
31+
const { message: contextMessage } = useMessageContext<StreamChatGenerics>('MessageTimestamp');
32+
const { Timestamp = DefaultTimestamp } = useComponentContext('MessageTimestamp');
4333
const message = propMessage || contextMessage;
44-
45-
const messageCreatedAt =
46-
message.created_at && isDate(message.created_at)
47-
? message.created_at.toISOString()
48-
: message.created_at;
49-
50-
const when = useMemo(
51-
() => getDateString({ calendar, format, formatDate, messageCreatedAt, tDateTimeParser }),
52-
[formatDate, calendar, tDateTimeParser, format, messageCreatedAt],
53-
);
54-
55-
if (!when) return null;
56-
57-
return (
58-
<time className={customClass} dateTime={messageCreatedAt} title={messageCreatedAt}>
59-
{when}
60-
</time>
61-
);
34+
return <Timestamp timestamp={message.created_at} {...timestampProps} />;
6235
};
6336

6437
export const MessageTimestamp = React.memo(

0 commit comments

Comments
Β (0)