diff --git a/src/components/Message/MessageEditedIndicator.tsx b/src/components/Message/MessageEditedIndicator.tsx new file mode 100644 index 0000000000..d8d37d2027 --- /dev/null +++ b/src/components/Message/MessageEditedIndicator.tsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import type { LocalMessage } from 'stream-chat'; +import type { TimestampFormatterOptions } from '../../i18n/types'; +import { PopperTooltip } from '../Tooltip'; +import { useEnterLeaveHandlers } from '../Tooltip/hooks'; +import { Timestamp as DefaultTimestamp } from './Timestamp'; +import { + useComponentContext, + useMessageContext, + useTranslationContext, +} from '../../context'; + +export type MessageEditedIndicatorProps = TimestampFormatterOptions & { + /* Adds a CSS class name to the component's outer container. */ + customClass?: string; + /* The `StreamChat` message object, which provides necessary data to the underlying UI components (overrides the value from `MessageContext`) */ + message?: LocalMessage; +}; + +const UnMemoizedMessageEditedIndicator = (props: MessageEditedIndicatorProps) => { + const { customClass, message: propMessage, ...timestampProps } = props; + const { message: contextMessage } = useMessageContext('MessageEditedIndicator'); + const { t } = useTranslationContext('MessageEditedIndicator'); + const { Timestamp = DefaultTimestamp } = useComponentContext('MessageEditedIndicator'); + const message = propMessage ?? contextMessage; + + const [referenceElement, setReferenceElement] = useState(null); + const { handleEnter, handleLeave, tooltipVisible } = + useEnterLeaveHandlers(); + + if (!message?.message_text_updated_at) { + return null; + } + + return ( + + {t('Edited')} + + + + + ); +}; + +export const MessageEditedIndicator = React.memo( + UnMemoizedMessageEditedIndicator, +) as typeof UnMemoizedMessageEditedIndicator; diff --git a/src/components/Message/MessageEditedTimestamp.tsx b/src/components/Message/MessageEditedTimestamp.tsx deleted file mode 100644 index 6ea9ac7721..0000000000 --- a/src/components/Message/MessageEditedTimestamp.tsx +++ /dev/null @@ -1,46 +0,0 @@ -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 { MessageTimestampProps } from './MessageTimestamp'; - -export type MessageEditedTimestampProps = MessageTimestampProps & { - open: boolean; -}; - -export function MessageEditedTimestamp({ - message: propMessage, - open, - ...timestampProps -}: MessageEditedTimestampProps) { - const { t } = useTranslationContext('MessageEditedTimestamp'); - const { message: contextMessage } = useMessageContext('MessageEditedTimestamp'); - const { Timestamp = DefaultTimestamp } = useComponentContext('MessageEditedTimestamp'); - const message = propMessage || contextMessage; - - if (!isMessageEdited(message)) { - return null; - } - - return ( -
- {t('Edited')}{' '} - -
- ); -} diff --git a/src/components/Message/MessageSimple.tsx b/src/components/Message/MessageSimple.tsx index 48dabd43c5..c1c2de11d7 100644 --- a/src/components/Message/MessageSimple.tsx +++ b/src/components/Message/MessageSimple.tsx @@ -9,6 +9,7 @@ import { MessageActions as DefaultMessageActions } from '../MessageActions'; import { MessageRepliesCountButton as DefaultMessageRepliesCountButton } from './MessageRepliesCountButton'; import { MessageStatus as DefaultMessageStatus } from './MessageStatus'; import { MessageText } from './MessageText'; +import { MessageEditedIndicator as DefaultMessageEditedIndicator } from './MessageEditedIndicator'; import { MessageTimestamp as DefaultMessageTimestamp } from './MessageTimestamp'; import { StreamedMessageText as DefaultStreamedMessageText } from './StreamedMessageText'; import { isDateSeparatorMessage } from '../MessageList'; @@ -39,12 +40,7 @@ import { useComponentContext } from '../../context/ComponentContext'; import type { MessageContextValue } from '../../context/MessageContext'; import { useMessageContext } from '../../context/MessageContext'; -import { - useChannelStateContext, - useChatContext, - useTranslationContext, -} from '../../context'; -import { MessageEditedTimestamp } from './MessageEditedTimestamp'; +import { useChannelStateContext, useChatContext } from '../../context'; import type { MessageUIComponentProps } from './types'; import { PinIndicator as DefaultPinIndicator } from './PinIndicator'; @@ -71,9 +67,7 @@ const MessageSimpleWithContext = ({ }: MessageSimpleWithContextProps) => { const { channel } = useChannelStateContext(); const { client } = useChatContext(); - const { t } = useTranslationContext(); const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false); - const [isEditedTimestampOpen, setEditedTimestampOpen] = useState(false); const reminder = useMessageReminder(message.id); const { @@ -85,6 +79,7 @@ const MessageSimpleWithContext = ({ MessageBouncePrompt = DefaultMessageBouncePrompt, MessageDeleted, MessageDeletedBubble = DefaultMessageDeletedBubble, + MessageEditedIndicator = DefaultMessageEditedIndicator, MessageRepliesCountButton = DefaultMessageRepliesCountButton, MessageStatus = DefaultMessageStatus, MessageTimestamp = DefaultMessageTimestamp, @@ -178,8 +173,6 @@ const MessageSimpleWithContext = ({ if (isBounced) { handleClick = () => setIsBounceDialogOpen(true); - } else if (isEdited) { - handleClick = () => setEditedTimestampOpen((prev) => !prev); } return ( @@ -257,10 +250,7 @@ const MessageSimpleWithContext = ({ )} - {isEdited && ( - {t('Edited')} - )} - {isEdited && } + {!isDeleted && isEdited && } )} diff --git a/src/components/Message/__tests__/MessageSimple.test.js b/src/components/Message/__tests__/MessageSimple.test.js index d789320e81..ee89e9424b 100644 --- a/src/components/Message/__tests__/MessageSimple.test.js +++ b/src/components/Message/__tests__/MessageSimple.test.js @@ -824,17 +824,5 @@ describe('', () => { const { queryAllByText } = await renderMessageSimple({ message }); expect(queryAllByText('Edited', { exact: true })).not.toHaveLength(0); }); - - it('should render open bounce modal on click', async () => { - const message = generateAliceMessage(editedMessageOptions); - const { getByTestId, queryByTestId } = await renderMessageSimple({ - message, - }); - fireEvent.click(getByTestId('message-inner')); - expect(queryByTestId('message-edited-timestamp')).toBeInTheDocument(); - expect(queryByTestId('message-edited-timestamp')).toHaveClass( - 'str-chat__message-edited-timestamp--open', - ); - }); }); }); diff --git a/src/components/Message/index.ts b/src/components/Message/index.ts index e58963f325..2fab043175 100644 --- a/src/components/Message/index.ts +++ b/src/components/Message/index.ts @@ -3,7 +3,6 @@ export * from './icons'; export * from './Message'; export * from './MessageBlocked'; export * from './MessageDeletedBubble'; -export * from './MessageEditedTimestamp'; export * from './MessageAlsoSentInChannelIndicator'; export * from './MessageRepliesCountButton'; export * from './PinIndicator'; @@ -11,6 +10,7 @@ export * from './MessageSimple'; export * from './MessageStatus'; export * from './MessageText'; export * from './MessageTimestamp'; +export * from './MessageEditedIndicator'; export * from './MessageTranslationIndicator'; export * from './QuotedMessage'; export * from './ReminderNotification'; diff --git a/src/components/Message/styling/Message.scss b/src/components/Message/styling/Message.scss index 71acf0cf18..f298cf0976 100644 --- a/src/components/Message/styling/Message.scss +++ b/src/components/Message/styling/Message.scss @@ -519,7 +519,7 @@ font: var(--str-chat__caption-medium-text); } - .str-chat__mesage-simple-edited { + .str-chat__message-edited-indicator { margin-left: var(--spacing-xs); } } diff --git a/src/components/Message/styling/MessageEditedTimestamp.scss b/src/components/Message/styling/MessageEditedTimestamp.scss deleted file mode 100644 index 34647a76e5..0000000000 --- a/src/components/Message/styling/MessageEditedTimestamp.scss +++ /dev/null @@ -1,13 +0,0 @@ -.str-chat__message-edited-timestamp { - overflow: hidden; - transition: height 0.1s; - flex-basis: 100%; - - &--open { - height: var(--str-chat__message-edited-timestamp-height, 1rem); - } - - &--collapsed { - height: 0; - } -} diff --git a/src/components/Message/styling/index.scss b/src/components/Message/styling/index.scss index 026268bbdf..9b83d6a795 100644 --- a/src/components/Message/styling/index.scss +++ b/src/components/Message/styling/index.scss @@ -1,6 +1,5 @@ @use 'Message'; @use 'MessageAlsoSentInChannelIndicator'; -@use 'MessageEditedTimestamp'; @use 'MessageIsThreadReplyInChannelButtonIndicator'; @use 'MessageStatus'; @use 'MessageSystem'; diff --git a/src/components/Thread/styling/ThreadHead.scss b/src/components/Thread/styling/ThreadHead.scss index 5196181d06..0f69a282f6 100644 --- a/src/components/Thread/styling/ThreadHead.scss +++ b/src/components/Thread/styling/ThreadHead.scss @@ -4,7 +4,7 @@ .str-chat__message { max-width: calc( var(--str-chat__message-composer-max-width) + - var(--str-chat__message-composer-padding) + var(--str-chat__message-composer-padding) ); padding-block: var(--spacing-xs); margin-inline: auto; diff --git a/src/components/TypingIndicator/styling/TypingIndicator.scss b/src/components/TypingIndicator/styling/TypingIndicator.scss index 436d71c961..08ff60f59a 100644 --- a/src/components/TypingIndicator/styling/TypingIndicator.scss +++ b/src/components/TypingIndicator/styling/TypingIndicator.scss @@ -18,7 +18,10 @@ display: flex; align-items: flex-end; width: 100%; - max-width: calc(var(--str-chat__message-composer-max-width) + var(--str-chat__message-composer-padding)); + max-width: calc( + var(--str-chat__message-composer-max-width) + + var(--str-chat__message-composer-padding) + ); margin: auto; gap: var(--spacing-xs); padding-block: var(--spacing-xs); diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index 6b765a72dc..9f9d663289 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -20,6 +20,7 @@ import { type LoadingIndicatorProps, type MessageBouncePromptProps, type MessageDeletedProps, + type MessageEditedIndicatorProps, type MessageInputProps, type MessageListNotificationsProps, type MessageProps, @@ -191,6 +192,8 @@ export type ComponentContextValue = { ReminderNotification?: React.ComponentType; /** Custom UI component to display the message translation indicator when message has i18n, defaults to and accepts same props as: [MessageTranslationIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/TranslationIndicator.tsx) */ MessageTranslationIndicator?: React.ComponentType; + /** Custom UI component to display the edited indicator and tooltip when a message has been edited, defaults to and accepts same props as: [MessageEditedIndicator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageEditedIndicator.tsx) */ + MessageEditedIndicator?: React.ComponentType; /** Custom component to display the search UI, defaults to and accepts same props as: [Search](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Search/Search.tsx) */ Search?: React.ComponentType; /** Custom component to display the UI where the searched string is entered, defaults to and accepts same props as: [SearchBar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Search/SearchBar/SearchBar.tsx) */