Skip to content

Commit

Permalink
feat: add message edited timestamp (#2304)
Browse files Browse the repository at this point in the history
### 🎯 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
  • Loading branch information
myandrienko authored Mar 7, 2024
1 parent 024ba6c commit 5633614
Show file tree
Hide file tree
Showing 26 changed files with 261 additions and 56 deletions.
16 changes: 13 additions & 3 deletions docusaurus/docs/React/components/contexts/component-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ Custom UI component to display attachment in an individual message.

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

| Type | Default |
| --------- | ----------------------------------------------------------------------------------- |
| Type | Default |
| --------- | ---------------------------------------------------------------------------------------------- |
| component | <GHComponentLink text='AttachmentPreviewList' path='/MessageInput/AttachmentPreviewList.tsx'/> |

### AutocompleteSuggestionHeader
Expand Down Expand Up @@ -256,12 +256,14 @@ Custom UI component to display system messages.

### MessageTimestamp

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

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

See also [`Timestamp`](#timestamp).

### MessageBouncePrompt

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

### Timestamp

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.

| Type | Default |
| --------- | ----------------------------------------------------------------- |
| component | <GHComponentLink text='Timestamp' path='/Message/Timestamp.tsx'/> |

### TriggerProvider

Optional context provider that lets you override the default autocomplete triggers.
Expand Down
26 changes: 18 additions & 8 deletions docusaurus/docs/React/components/core-components/channel.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ Custom UI component to display a message attachment.

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

| Type | Default |
| --------- | ----------------------------------------------------------------------------------- |
| Type | Default |
| --------- | ---------------------------------------------------------------------------------------------- |
| component | <GHComponentLink text='AttachmentPreviewList' path='/MessageInput/AttachmentPreviewList.tsx'/> |

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

A custom function to provide size configuration for image attachments

| Type |
| ---------------------------------------------------------------- |
| Type |
| ----------------------------------------------------------------- |
| `(a: Attachment, e: HTMLElement) => ImageAttachmentConfiguration` |

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

| Type | Default |
| ------- | ------- |
| boolean | true |
| boolean | true |

### Input

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

Custom component to render link previews in `MessageInput`.

| Type | Default |
| --------- | ----------------------------------------------------------------------------------- |
| Type | Default |
| --------- | ---------------------------------------------------------------------------------- |
| component | <GHComponentLink text='LinkPreviewList' path='/MessageInput/LinkPreviewList.tsx'/> |

### LoadingErrorIndicator
Expand Down Expand Up @@ -553,12 +553,14 @@ Custom UI component to display system messages.

### MessageTimestamp

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

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

See also [`Timestamp`](#timestamp).

### MessageBouncePrompt

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

### Timestamp

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.

| Type | Default |
| --------- | ----------------------------------------------------------------- |
| component | <GHComponentLink text='Timestamp' path='/Message/Timestamp.tsx'/> |

### TriggerProvider

Optional context provider that lets you override the default autocomplete triggers.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ The following UI components are available for use:
- [`QuotedMessage`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/QuotedMessage.tsx) - shows a quoted
message UI wrapper when the sent message quotes a previous message

- [`Timestamp`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/Timestmap.tsx) - formats and displays a date,
used by `MessageTimestamp` and for edited message timestamps.

- [`MessageBouncePrompt`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageBounce/MessageBouncePrompt.tsx) -
presents options to deal with a message that got bounced by the moderation rules.

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

## MessageTimestamp Props

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`.

### calendar

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

## MessageBouncePrompt
## MessageBouncePrompt props

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

Expand Down Expand Up @@ -460,3 +465,41 @@ The Message UI component will pass this callback to close the modal dialog `Mess
| Type |
| ----------------- |
| ReactEventHandler |

## Timestamp props

### calendar

If true, call the `Day.js` calendar function to get the date string to display.

| Type | Default |
| ------- | ------- |
| boolean | false |

### customClass

If provided, adds a CSS class name to the component's outer `time` container.

```jsx
<time className={customClass} />
```

| Type |
| ------ |
| string |

### format

If provided, overrides the default timestamp format.

| Type | Default |
| ------ | ------- |
| string | 'h:mmA' |

### timestamp

Either an ISO string with a date, or a Date object with a date to display.

| Type |
| -------------- |
| Date \| string |
7 changes: 4 additions & 3 deletions docusaurus/docs/React/guides/theming/message-ui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ The custom Message UI component built below imports and uses the following UI co
- [`SimpleReactionsList`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Reactions/SimpleReactionsList.tsx) - displays
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)).

- [`Timestamp`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/Timestmap.tsx) - formats and displays a date,
used by `MessageTimestamp` and for edited message timestamps.

### How it Fits Together

The sample code below assembles the above UI building blocks into a fully featured Message UI component. The UI components allow you to
Expand Down Expand Up @@ -105,9 +108,7 @@ export const CustomMessage = () => {
<MessageTimestamp />
</div>
</div>
{showDetailedReactions && canReact && (
<ReactionSelector ref={reactionSelectorRef} />
)}
{showDetailedReactions && canReact && <ReactionSelector ref={reactionSelectorRef} />}
<MessageText />
<MessageStatus />
{hasAttachments && <Attachment attachments={message.attachments} />}
Expand Down
3 changes: 3 additions & 0 deletions src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ type ChannelPropsForwardedToComponentContext<
ThreadHeader?: ComponentContextValue<StreamChatGenerics>['ThreadHeader'];
/** 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) */
ThreadStart?: ComponentContextValue<StreamChatGenerics>['ThreadStart'];
/** 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. */
Timestamp?: ComponentContextValue<StreamChatGenerics>['Timestamp'];
/** 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) */
TriggerProvider?: ComponentContextValue<StreamChatGenerics>['TriggerProvider'];
/** 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) */
Expand Down Expand Up @@ -1192,6 +1194,7 @@ const ChannelInner = <
ThreadHead: props.ThreadHead,
ThreadHeader: props.ThreadHeader,
ThreadStart: props.ThreadStart,
Timestamp: props.Timestamp,
TriggerProvider: props.TriggerProvider,
TypingIndicator: props.TypingIndicator,
UnreadMessagesNotification: props.UnreadMessagesNotification,
Expand Down
49 changes: 49 additions & 0 deletions src/components/Message/MessageEditedTimestamp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react';

import clsx from 'clsx';
import { useComponentContext, useMessageContext, useTranslationContext } from '../../context';
import { Timestamp as DefaultTimestamp } from './Timestamp';
import { isMessageEdited } from './utils';

import type { DefaultStreamChatGenerics } from '../../types';
import type { MessageTimestampProps } from './MessageTimestamp';

export type MessageEditedTimestampProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
> = MessageTimestampProps<StreamChatGenerics> & {
open: boolean;
};

export function MessageEditedTimestamp<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>({
message: propMessage,
open,
...timestampProps
}: MessageEditedTimestampProps<StreamChatGenerics>) {
const { t } = useTranslationContext('MessageEditedTimestamp');
const { message: contextMessage } = useMessageContext<StreamChatGenerics>(
'MessageEditedTimestamp',
);
const { Timestamp = DefaultTimestamp } = useComponentContext('MessageEditedTimestamp');
const message = propMessage || contextMessage;

if (!isMessageEdited(message)) {
return null;
}

return (
<div
className={clsx(
'str-chat__message-edited-timestamp',
open
? 'str-chat__message-edited-timestamp--open'
: 'str-chat__message-edited-timestamp--collapsed',
)}
data-testid='message-edited-timestamp'
>
{t<string>('Edited')}{' '}
<Timestamp timestamp={message.message_text_updated_at} {...timestampProps} />
</div>
);
}
12 changes: 12 additions & 0 deletions src/components/Message/MessageSimple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { MessageTimestamp as DefaultMessageTimestamp } from './MessageTimestamp'
import {
areMessageUIPropsEqual,
isMessageBounced,
isMessageEdited,
messageHasAttachments,
messageHasReactions,
} from './utils';
Expand All @@ -34,6 +35,8 @@ import { MessageContextValue, useMessageContext } from '../../context/MessageCon
import type { MessageUIComponentProps } from './types';

import type { DefaultStreamChatGenerics } from '../../types/types';
import { useTranslationContext } from '../../context';
import { MessageEditedTimestamp } from './MessageEditedTimestamp';

type MessageSimpleWithContextProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
Expand Down Expand Up @@ -66,7 +69,9 @@ const MessageSimpleWithContext = <
threadList,
} = props;

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

const {
Attachment,
Expand Down Expand Up @@ -106,13 +111,16 @@ const MessageSimpleWithContext = <
const showReplyCountButton = !threadList && !!message.reply_count;
const allowRetry = message.status === 'failed' && message.errorStatusCode !== 403;
const isBounced = isMessageBounced(message);
const isEdited = isMessageEdited(message);

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

if (allowRetry) {
handleClick = () => handleRetry(message);
} else if (isBounced) {
handleClick = () => setIsBounceDialogOpen(true);
} else if (isEdited) {
handleClick = () => setEditedTimestampOpen((prev) => !prev);
}

const rootClassName = clsx(
Expand Down Expand Up @@ -228,6 +236,10 @@ const MessageSimpleWithContext = <
</span>
)}
<MessageTimestamp calendar customClass='str-chat__message-simple-timestamp' />
{isEdited && (
<span className='str-chat__mesage-simple-edited'>{t<string>('Edited')}</span>
)}
{isEdited && <MessageEditedTimestamp calendar open={isEditedTimestampOpen} />}
</div>
)}
</div>
Expand Down
45 changes: 9 additions & 36 deletions src/components/Message/MessageTimestamp.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React, { useMemo } from 'react';

import { useMessageContext } from '../../context/MessageContext';
import { isDate, useTranslationContext } from '../../context/TranslationContext';
import React from 'react';

import type { StreamMessage } from '../../context/ChannelStateContext';

import type { DefaultStreamChatGenerics } from '../../types/types';
import { getDateString } from '../../i18n/utils';

import { useMessageContext } from '../../context/MessageContext';
import { Timestamp as DefaultTimestamp } from './Timestamp';
import { useComponentContext } from '../../context';

export const defaultTimestampFormat = 'h:mmA';

Expand All @@ -28,37 +27,11 @@ const UnMemoizedMessageTimestamp = <
>(
props: MessageTimestampProps<StreamChatGenerics>,
) => {
const {
calendar = false,
customClass = '',
format = defaultTimestampFormat,
message: propMessage,
} = props;

const { formatDate, message: contextMessage } = useMessageContext<StreamChatGenerics>(
'MessageTimestamp',
);
const { tDateTimeParser } = useTranslationContext('MessageTimestamp');

const { message: propMessage, ...timestampProps } = props;
const { message: contextMessage } = useMessageContext<StreamChatGenerics>('MessageTimestamp');
const { Timestamp = DefaultTimestamp } = useComponentContext('MessageTimestamp');
const message = propMessage || contextMessage;

const messageCreatedAt =
message.created_at && isDate(message.created_at)
? message.created_at.toISOString()
: message.created_at;

const when = useMemo(
() => getDateString({ calendar, format, formatDate, messageCreatedAt, tDateTimeParser }),
[formatDate, calendar, tDateTimeParser, format, messageCreatedAt],
);

if (!when) return null;

return (
<time className={customClass} dateTime={messageCreatedAt} title={messageCreatedAt}>
{when}
</time>
);
return <Timestamp timestamp={message.created_at} {...timestampProps} />;
};

export const MessageTimestamp = React.memo(
Expand Down
Loading

0 comments on commit 5633614

Please sign in to comment.