From a951519ca010e459b6ba15b2c090fb4292c27f44 Mon Sep 17 00:00:00 2001 From: martincupela Date: Wed, 4 Nov 2026 00:11:40 +0100 Subject: [PATCH 01/11] fix!: drop unset MessageContext fields and unify message grouping props MessageContext declared threadList, firstOfGroup, endOfGroup and groupedByUser, but no message list set them. groupStyles is now the single source of grouping, and thread detection is useThreadContext(). - Remove threadList from MessageProps and MessageContextValue. ReactionSelector.getDialogId takes only { messageId }; each list's own dialog manager keeps a message's channel and thread dialogs apart. - Remove endOfGroup, firstOfGroup and groupedByUser from MessageContext and the str-chat__virtual-message__wrapper--* classes. The default MessageUI renders the metadata on every message and the str-chat__li--top/--middle CSS hides it. - Rename MessageList.noGroupByUser and VirtualizedMessageList.shouldGroupByUser to groupByUser (defaults true and false), and the groupStyles function prop to getGroupStyles, called with one GetGroupStylesParams object. - Share the group-style loop as getMessageGroupStyles. The virtualized list now recomputes group styles when a message changes, not only when the count does. BREAKING CHANGE: MessageProps.threadList, MessageContextValue.threadList, endOfGroup, firstOfGroup and groupedByUser are removed, as are the str-chat__virtual-message__wrapper--* classes. noGroupByUser and shouldGroupByUser are replaced by groupByUser, the groupStyles prop by getGroupStyles, and getGroupStyles and useEnrichedMessages take objects with groupByUser. See BC-079 and BC-080 in ai-docs/v15-breaking-changes.md. Ticket: https://linear.app/stream/issue/REACT-1278 --- AGENTS.md | 2 +- ai-docs/ai-migration-v14-v15.md | 48 +++++- ai-docs/v15-breaking-changes.md | 56 ++++++- .../Attachment/__tests__/Audio.test.tsx | 5 +- .../__tests__/VoiceRecording.test.tsx | 8 +- src/components/Message/Message.tsx | 1 - src/components/Message/MessageUI.tsx | 29 ++-- .../__tests__/MessageBubbleContent.test.tsx | 2 +- .../Message/__tests__/MessageText.test.tsx | 1 - .../Message/__tests__/MessageUI.test.tsx | 25 ++- src/components/Message/types.ts | 9 +- src/components/Message/utils.tsx | 1 - .../MessageActions.defaults.tsx | 10 +- .../MessageActions/MessageActions.tsx | 3 +- .../__tests__/MessageActions.test.tsx | 3 +- src/components/MessageList/MessageList.tsx | 24 ++- .../MessageList/VirtualizedMessageList.tsx | 61 +++---- .../__tests__/MessageList.test.tsx | 143 +++++++++++++++- .../__tests__/VirtualizedMessageList.test.tsx | 64 ++++++++ .../VirtualizedMessageListComponents.test.tsx | 93 +---------- .../MessageList/__tests__/utils.test.ts | 153 ++++++++++++------ .../hooks/MessageList/useEnrichedMessages.ts | 39 ++--- src/components/MessageList/utils.ts | 73 ++++++++- src/components/Reactions/ReactionSelector.tsx | 14 +- .../Reactions/ReactionSelectorWithButton.tsx | 7 +- .../ReactionSelectorWithButton.test.tsx | 7 +- src/context/MessageContext.tsx | 15 +- 27 files changed, 579 insertions(+), 317 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 04bbdc424..856266767 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -221,7 +221,7 @@ Date separators come from `withDateSeparator`: on by default in `MessageList`, o ### 3. Memoization - `useStateStore` selectors scope re-renders: an over-broad or unstable slice re-renders everything that reads it. -- `areMessageUIPropsEqual` (`src/components/Message/utils.tsx`) is the per-message `React.memo` comparator. It checks cheap props first (`highlighted`, `endOfGroup`, `readBy.length`, `deliveredTo.length`, `groupStyles`, `showDetailedReactions`, `lastReceivedId`) before comparing messages. A prop it does not check will not re-render a message. +- `areMessageUIPropsEqual` (`src/components/Message/utils.tsx`) is the per-message `React.memo` comparator. It checks cheap props first (`highlighted`, `readBy.length`, `deliveredTo.length`, `groupStyles`, `showDetailedReactions`, `lastReceivedId`) before comparing messages. A prop it does not check will not re-render a message. ## Critical gotchas & invariants diff --git a/ai-docs/ai-migration-v14-v15.md b/ai-docs/ai-migration-v14-v15.md index a74472d63..dda15e982 100644 --- a/ai-docs/ai-migration-v14-v15.md +++ b/ai-docs/ai-migration-v14-v15.md @@ -1871,7 +1871,8 @@ Code that drove these props uses the paginator: - `MessageList.threadList`, `VirtualizedMessageList.threadList`, `VirtuosoContext.threadList`, `TypingIndicatorProps.threadList` and `ScrollToLatestMessageButtonProps.threadList` are removed. The SDK no longer passes `threadList` to custom `TypingIndicator` / `ScrollToLatestMessageButton` overrides; in untyped code it is `undefined`, so a thread-aware override behaves as if it were in the channel list. Detect the thread with `const isThreadList = !!useThreadContext();`. - `MessageList.head` and `VirtualizedMessageList.head` are removed. Both lists render the parent message from the thread context. Override it with a `ThreadHead` slot (`ComponentType`). On `VirtualizedMessageList`, `additionalVirtuosoProps.components.Header` still replaces the whole header slot. -- `Message` / `MessageContext` still have a `threadList` field; only the list-level prop is gone. +- `MessageProps.threadList` and `MessageContextValue.threadList` are removed too. A custom `MessageUI` or message child that read `useMessageContext().threadList` uses `!!useThreadContext()` instead. +- `ReactionSelector.getDialogId` takes only `{ messageId }`, and the reaction selector's dialog id no longer has a `-thread` part. The channel list and the thread list each have their own dialog manager, so the same message open in both still gets two separate dialogs. ### `disableDateSeparator` → `withDateSeparator` @@ -1903,6 +1904,41 @@ export const Lists = () => ( ); ``` +### Message grouping: `groupByUser` and `getGroupStyles` + +Both lists take `groupByUser`; the defaults are unchanged. + +| Component | v14 | v15 | +| ------------------------ | ------------------------------------- | ------------------------------- | +| `MessageList` | `noGroupByUser` (default `false`) | `groupByUser` (default `true`) | +| `VirtualizedMessageList` | `shouldGroupByUser` (default `false`) | `groupByUser` (default `false`) | + +The `groupStyles` prop of both lists is renamed `getGroupStyles`, and the function takes one object instead of five positional arguments: `{ groupByUser, maxTimeBetweenGroupedMessages, message, nextMessage, previousMessage }` (`GetGroupStylesParams`; `nextMessage` and `previousMessage` are `undefined` at the ends of the list). `groupByUser` replaces the inverted `noGroupByUser` argument. The exported default `getGroupStyles` takes the same object, so a custom function can still delegate to it. `useEnrichedMessages` takes `getGroupStyles` and `groupByUser` instead of `groupStyles` and `noGroupByUser`. `VirtuosoContext.shouldGroupByUser` is `groupByUser`. + +In untyped code the old props are ignored and the defaults apply: `MessageList noGroupByUser` groups again, `VirtualizedMessageList shouldGroupByUser` stops grouping, and a `groupStyles` function is never called. A positional function passed as `getGroupStyles` receives the object as its first argument and computes wrong styles without an error; TypeScript rejects it. + +```tsx +// v14 + + + + 'pinned' in message && message.pinned + ? 'single' + : getGroupStyles(message, previousMessage, nextMessage, noGroupByUser, maxTime) + } +/> + +// v15 + + + + 'pinned' in params.message && params.message.pinned ? 'single' : getGroupStyles(params) + } +/> +``` + ### Custom `renderMessages` gets different options - `RenderMessagesOptions.channel: Channel` is new and required. @@ -1914,6 +1950,7 @@ export const Lists = () => ( - Removed: `Message`, `messageActions`, `openThread`, `threadList`. - New and required: `channel: Channel`. New: `focusedMessageId?`. +- Renamed: `shouldGroupByUser` → `groupByUser`. - `head`, `firstUnreadMessageId` and `lastReadMessageId` are `... | null` (checks written as `=== undefined` miss them); `lastReadDate` is `TimestampNS | null` (unix nanoseconds); `loadingMore: boolean`; `unreadMessageCount: number`. ### `headerPosition` and `insertIntro` take unix nanoseconds @@ -2016,6 +2053,10 @@ export const MessagePreview = ({ - `mutes` → `useChatContext().mutes` (now `UserMuteResponse[]`), for example `isUserMuted(message, useChatContext().mutes)`. In untyped code the removed field is `undefined`, so `isUserMuted(message, mutes)` returns `false`. - `messageIsUnread` has no successor. Derive it from the channel's read state, for example `!isMyMessage() && (channel.lastRead() == null || message.created_at > channel.lastRead())`, with `channel` from `useChannel()`. `channel.lastRead()` returns `TimestampNS | null | undefined` and is not reactive; to re-render on read changes, subscribe to `read` on `channel.state`. +### `MessageContext` grouping flags are removed; read `groupStyles` + +`endOfGroup`, `firstOfGroup` and `groupedByUser` are removed from `MessageContextValue`. The message lists never set them, so in v14 they were always `undefined`. A message's place in its group of consecutive messages from one user is `groupStyles[0]` from `useMessageContext()`: `top`, `middle`, `bottom` or `single`. The default `MessageUI` renders the metadata on every message, and the default CSS hides it on `top` and `middle` list items (`.str-chat__li--top`, `.str-chat__li--middle`), so an app can show it again with CSS alone. The `str-chat__virtual-message__wrapper--end`, `--first` and `--group` classes these flags added are removed too ([section 19](#19-css-and-markup)). + ### `retrySendMessage` and `useRetryHandler` - The `retrySendMessage` prop on `Message`, `MessageList` and `SharedMessageProps` is removed. To change the retry request, register `retrySendMessageRequest` on the client ([section 14](#14-request-handlers)). @@ -2812,6 +2853,7 @@ Four formatter keys carry English day words inside their `calendarFormats` (`tim | `.str-chat__channel-list-inner`, `.str-chat__channel-list-inner__main` (from `ChannelListUI`, which held `role="listbox"`) | Gone. Target `.str-chat__channel-list .str-chat__infinite-scroll-paginator` (the `role="listbox"` scroll container, inside `ChannelNavigation`) and `.str-chat__infinite-scroll-paginator__content`. A bare `` has no `.str-chat__channel-list` ancestor. | | The channel list root carried `str-chat str-chat__channel-list` and `customClasses.channelList` | Inside `ChatView` the `str-chat ` scope comes from the `ChatView` root, and the channel list adds none of its own: a nested `.str-chat` would reset the tokens you override on the `ChatView` root. Outside `ChatView`, `ChannelNavigation`'s root renders `str-chat str-chat__channel-list`, and a bare `` wraps itself in `div.str-chat..str-chat__theme-scope` (`display: contents`). `customClasses.chat` replaces `str-chat` where the scope is applied; `customClasses.channelList` replaces `str-chat__channel-list` on `ChannelNavigation`'s root. App CSS that targeted the list root as a `.str-chat` element no longer matches it inside `ChatView`. | | `.str-chat__chat-view__channels` / `.str-chat__chat-view__threads` | Rendered only when `ChatView` is given `views`. With the built-in workspace layout, content sits in `.str-chat__chat-view__workspace-layout`, `.str-chat__chat-view__workspace-layout-slots`, `.str-chat__chat-view__slot` (`--hidden`) and `.str-chat__chat-view__slot-layer`. | +| `str-chat__virtual-message__wrapper--end`, `--first`, `--group` on `.str-chat__message` | Not applied; the SDK stylesheet never styled them. Target the list item's group class instead: `.str-chat__li--top`, `--middle`, `--bottom` or `--single`. | | `.str-chat__thread--virtualized` on the thread container | Not applied. Scope by the list you render (`.str-chat__thread .str-chat__virtual-list`, or your own class). | | `.str-chat__quoted-message-preview` text inherited the surrounding colour | It sets `color: var(--str-chat__chat-text-incoming)`, and `--own` `var(--str-chat__chat-text-outgoing)`, matching the bubble backgrounds. In the default light theme your own quotes change to the outgoing bubble's text colour. Remove contrast workarounds written for v14; theme with those two tokens. | | `.str-chat__channel-list-item[aria-pressed=true]` and `.str-chat__thread-list-item[aria-pressed=true]` highlighted the active row | Only `[aria-selected=true]` is styled. A custom row UI sets `role="option"` and `aria-selected`, not `aria-pressed`: `aria-selected={active}` on a channel row, `aria-selected={isThreadActive(thread.id)}` (from `useWorkspaceNavigation()`) on a thread row | @@ -2819,7 +2861,7 @@ Four formatter keys carry English day words inside their `calendarFormats` (`tim Find every app rule that uses a removed class. They also hide inside combinators and pseudo-classes, where a rule does more than style the element: `.create-channel-panel + .str-chat__main-panel { display: none }` hid the channel while another panel was open, and without it both render side by side. Nothing reports such a rule; it simply stops matching. ```bash -grep -rnE "str-chat__(main-panel(--[a-z-]+)?|container|channel-list-inner(__main)?|thread--virtualized)([^a-z0-9_-]|$)" \ +grep -rnE "str-chat__(main-panel(--[a-z-]+)?|container|channel-list-inner(__main)?|thread--virtualized|virtual-message__wrapper--(end|first|group))([^a-z0-9_-]|$)" \ --include='*.css' --include='*.scss' --include='*.sass' --include='*.less' \ --exclude-dir=node_modules --exclude-dir=build --exclude-dir=dist . ``` @@ -2956,3 +2998,5 @@ On narrow screens (`max-width: 767px`) the lists switch to an overlay sized by ` | BC-075 | `ChatView` view wrappers | [19](#19-css-and-markup) | | BC-076 | `str-chat__thread--virtualized` | [19](#19-css-and-markup) | | BC-077 | Quote and edit preview colours | [19](#19-css-and-markup) | +| BC-079 | `MessageContext` grouping flags and `virtual-message__wrapper--*` classes | [12](#messagecontext-grouping-flags-are-removed-read-groupstyles), [19](#19-css-and-markup) | +| BC-080 | `groupByUser`, `getGroupStyles` | [11](#message-grouping-groupbyuser-and-getgroupstyles) | diff --git a/ai-docs/v15-breaking-changes.md b/ai-docs/v15-breaking-changes.md index daa58bbee..9bded7091 100644 --- a/ai-docs/v15-breaking-changes.md +++ b/ai-docs/v15-breaking-changes.md @@ -144,6 +144,8 @@ yarn api-surface check --doc ai-docs/v15-breaking-changes.md --diff diff.json | [BC-075](#bc-075) | CSS, ChatView: `str-chat__chat-view__channels` / `__threads` render only in `views` mode | CSS and markup | | [BC-076](#bc-076) | CSS, Thread: `str-chat__thread--virtualized` is no longer applied | CSS and markup | | [BC-077](#bc-077) | quote and edit previews set their text colour from the matching bubble token | CSS and markup | +| [BC-079](#bc-079) | `MessageContext` grouping flags `endOfGroup` / `firstOfGroup` / `groupedByUser` and the `str-chat__virtual-message__wrapper--*` classes are removed | Messages and message lists | +| [BC-080](#bc-080) | message grouping: `groupByUser` replaces `noGroupByUser` / `shouldGroupByUser`; `getGroupStyles` replaces `groupStyles` and takes one object | Messages and message lists | ## Package and runtime @@ -2332,13 +2334,16 @@ export const ChannelWithThread = ({ channel }: { channel: StreamChannel }) => { - Area: MessageList / VirtualizedMessageList / TypingIndicator / ScrollToLatestMessageButton - User impact: - compile errors on `threadList` / `head` props, and in custom `TypingIndicator` / `ScrollToLatestMessageButton` overrides that destructure `threadList`, which the SDK no longer passes + - compile errors in custom `MessageUI` or message children that read `threadList` from `useMessageContext()`, and on `ReactionSelector.getDialogId({ messageId, threadList })` - in untyped overrides, `threadList` is `undefined`, so a thread-aware custom component behaves as if it were in the channel list - Old API: - `MessageList.threadList`, `VirtualizedMessageList.threadList`, `VirtuosoContext.threadList` - `TypingIndicatorProps.threadList`, `ScrollToLatestMessageButtonProps.threadList` + - `MessageProps.threadList`, `MessageContextValue.threadList`; `ReactionSelector.getDialogId`'s `threadList` argument and the `-thread` part of the reaction selector dialog id - `MessageList.head`, `VirtualizedMessageList.head` (the thread's parent message element) - Replacement: - - thread detection: `const thread = useThreadContext(); const isThreadList = !!thread;` (the SDK's own components do this) + - thread detection: `const thread = useThreadContext(); const isThreadList = !!thread;` (the SDK's own components do this), also in place of `useMessageContext().threadList` + - reaction selector dialog id: `ReactionSelector.getDialogId({ messageId })`. Each message list has its own dialog manager, which keeps the channel's and the thread's dialogs for one message apart. - parent message: both lists render it from `ThreadContext` via `useThreadHead()`. Override it with the `ThreadHead` slot: ``, typed `React.ComponentType`. - `VirtuosoContext.head` is now `ReactElement | null` and always present - Evidence: @@ -2353,7 +2358,7 @@ export const ChannelWithThread = ({ channel }: { channel: StreamChannel }) => { - Docs impact: - `02-ui-components/07-message-list/01-message_list.md`, `02-ui-components/07-message-list/02-virtualized_list.md` (`threadList`, `head`) - `02-ui-components/12-indicators.md`, `03-ui-cookbook/05-message-composer/07-typing_indicator.md` (`threadList`) - - `02-ui-components/08-message/01-message.md`, `02-ui-components/08-message/02-message_context.md`, `03-ui-cookbook/04-message/01-message_ui.md` mention `threadList`, which is still a `Message` / `MessageContext` field; only the list-level prop is gone, so check each mention + - `02-ui-components/08-message/01-message.md`, `02-ui-components/08-message/02-message_context.md`, `03-ui-cookbook/04-message/01-message_ui.md` mention `threadList` as a `Message` / `MessageContext` field, which is removed too; replace each mention with `useThreadContext()` - Example needed: no (see BC-004 for a custom `TypingIndicator`) ### BC-049: `disableDateSeparator` renamed to `withDateSeparator` (inverted) @@ -3275,6 +3280,49 @@ export const DeleteForMe = () => { - `02-ui-components/02-theming/01-themingv2.md` (line 152 text-colour note) - Example needed: no (the guide section is accurate) +### BC-079: `MessageContext` grouping flags and the `str-chat__virtual-message__wrapper--*` classes are removed + +- Status: confirmed +- Area: Message / MessageUI / CSS +- User impact: + - compile errors in custom `MessageUI` or message children that read `endOfGroup`, `firstOfGroup` or `groupedByUser` from `useMessageContext()`. The lists never set them, so at runtime they were already `undefined`. + - app CSS keyed on `str-chat__virtual-message__wrapper--end`, `--first` or `--group` stops matching. The SDK stylesheet never styled them. +- Old API: + - `MessageContextValue.endOfGroup`, `.firstOfGroup`, `.groupedByUser` + - `str-chat__virtual-message__wrapper--end`, `--first`, `--group` on `.str-chat__message` +- Replacement: + - `useMessageContext().groupStyles[0]`: `top`, `middle`, `bottom` or `single` + - CSS: the list item's `.str-chat__li--top`, `--middle`, `--bottom`, `--single`. The default `MessageUI` renders the metadata on every message, and the default CSS hides it on `top` and `middle` items. +- Evidence: + - `release-v14:src/components/Message/MessageUI.tsx:144` `showMetadata = !groupedByUser || endOfGroup`, with no list setting either flag + - `master:src/components/Message/styling/Message.scss` `.str-chat__li--middle, .str-chat__li--top` hide `.str-chat__message-metadata` +- Docs impact: + - `02-ui-components/08-message/02-message_context.md` and `03-ui-cookbook/04-message/01-message_ui.md`, if they mention the three flags +- Example needed: no + +### BC-080: message grouping: `noGroupByUser` / `shouldGroupByUser` → `groupByUser`, `groupStyles` → `getGroupStyles` with one object argument + +- Status: confirmed +- Area: MessageList / VirtualizedMessageList / getGroupStyles / useEnrichedMessages +- User impact: + - compile errors on `noGroupByUser`, `shouldGroupByUser` and `groupStyles`, on positional calls of `getGroupStyles`, and on the old `useEnrichedMessages` options + - in untyped code the old props are ignored and the defaults apply: `MessageList noGroupByUser` groups again, `VirtualizedMessageList shouldGroupByUser` stops grouping, and a `groupStyles` function is never called. A positional function passed as `getGroupStyles` receives the object as `message` and computes wrong styles without an error. +- Old API: + - `MessageList.noGroupByUser` (default `false`), `VirtualizedMessageList.shouldGroupByUser` (default `false`), `VirtuosoContext.shouldGroupByUser` + - `MessageList.groupStyles`, `VirtualizedMessageList.groupStyles`: `(message, previousMessage, nextMessage, noGroupByUser, maxTimeBetweenGroupedMessages?) => GroupStyle` + - `getGroupStyles(message, previousMessage, nextMessage, noGroupByUser, maxTimeBetweenGroupedMessages?)` + - `useEnrichedMessages({ groupStyles, noGroupByUser })` +- Replacement: + - `groupByUser` on both lists. `MessageList` defaults to `true`, `VirtualizedMessageList` to `false`. `VirtuosoContext.groupByUser`. + - `getGroupStyles?: GetGroupStyles` on both lists, called with `GetGroupStylesParams`: `{ groupByUser, maxTimeBetweenGroupedMessages?, message, nextMessage?, previousMessage? }`. The exported `getGroupStyles` takes the same object. + - `useEnrichedMessages({ getGroupStyles, groupByUser })` +- Evidence: + - `release-v14:src/components/MessageList/MessageList.tsx:105` `noGroupByUser = false`, `:517` positional `groupStyles`, `:555` `noGroupByUser?` + - `release-v14:src/components/MessageList/VirtualizedMessageList.tsx:236` `shouldGroupByUser = false`, `:102` picked into the context, `:693` positional `groupStyles` +- Docs impact: + - `02-ui-components/07-message-list/01-message_list.md`, `02-ui-components/07-message-list/02-virtualized_list.md` +- Example needed: no (the guide section has one) + ## Pending (open PRs) - `GetStream/stream-chat-react#3306` `feat: adopt TS generator changes` (open, targets `master`). It changes the request shapes for sending, editing and deleting messages, reactions, reminders and poll-option suggestions to follow `GetStream/stream-chat-js#1896`. If it merges before the stable release, re-run the surface diff and update the request-handler entries (search this file for `sendMessageRequest`). @@ -3603,6 +3651,7 @@ Non-gated items checked and ruled out: `Chat.isMessageAIGenerated`, `SummarizedM - `getIsFirstUnreadMessage` -> BC-054 - `insertIntro` -> BC-056 - `MessageList.disableDateSeparator`, `MessageListProps.disableDateSeparator`, `ProcessMessagesParams.enableDateSeparator` -> BC-049 + - `MessageList.noGroupByUser`, `MessageList.groupStyles` (+ `MessageListProps`), `getGroupStyles` signature, `useEnrichedMessages` options -> BC-080 - `MessageList.head`, `MessageListProps.head`, `MessageList.threadList`, `MessageListProps.threadList` -> BC-048 - `MessageList.loadingMoreForJumpToChannelMessage`, `.loadMore`, `.loadMoreNewer` (+ `MessageListProps`) -> BC-046 - `MessageList.Message`, `MessageListProps.Message`, `SharedMessageProps.Message` -> BC-042 @@ -3616,6 +3665,7 @@ Non-gated items checked and ruled out: `Chat.isMessageAIGenerated`, `SummarizedM - `useEnrichedMessages`, `useNewMessageNotification`, `UseScrollLocationLogicParams.disableAutoScrollToBottom` / `.disableScrollManagement` -> BC-055 - `VirtualizedMessageList.channelUnreadUiState` / `hasMore` / `hasMoreNewer` / `highlightedMessageId` / `loadingMore` / `loadingMoreNewer` / `loadMore` / `loadMoreNewer` / `messageLimit` (+ `VirtualizedMessageListProps`) -> BC-046 - `VirtualizedMessageList.disableDateSeparator` (+ Props) -> BC-049 + - `VirtualizedMessageList.shouldGroupByUser`, `VirtualizedMessageList.groupStyles` (+ Props), `VirtuosoContext.shouldGroupByUser` -> BC-080 - `VirtualizedMessageList.head`, `.threadList` (+ Props) -> BC-048 - `VirtualizedMessageList.Message` (+ Props) -> BC-042 - `VirtualizedMessageList.messageActions` (+ Props) -> BC-041 @@ -3752,7 +3802,7 @@ Non-gated items checked and ruled out: `Chat.isMessageAIGenerated`, `SummarizedM #### diff.md "CSS" -- Classes removed from markup: `str-chat__main-panel`, `str-chat__main-panel---with-thread-opn`, `str-chat__main-panel--thread-open` -> BC-073; `str-chat__channel-list-inner`, `str-chat__channel-list-inner__main` -> BC-074; `str-chat__chat-view__channels`, `str-chat__chat-view__threads` -> BC-075 (partially a false positive); `str-chat__thread--virtualized` -> BC-076 +- Classes removed from markup: `str-chat__main-panel`, `str-chat__main-panel---with-thread-opn`, `str-chat__main-panel--thread-open` -> BC-073; `str-chat__channel-list-inner`, `str-chat__channel-list-inner__main` -> BC-074; `str-chat__chat-view__channels`, `str-chat__chat-view__threads` -> BC-075 (partially a false positive); `str-chat__thread--virtualized` -> BC-076; `str-chat__virtual-message__wrapper--end`, `--first`, `--group` -> BC-079 - Classes removed from stylesheets (diff reports 0): actually `str-chat__container`, `str-chat__main-panel` -> BC-073 - Custom properties removed: 0 (confirmed by own diff of `--str-chat*` declarations) diff --git a/src/components/Attachment/__tests__/Audio.test.tsx b/src/components/Attachment/__tests__/Audio.test.tsx index 2830bfd57..ce9a817b7 100644 --- a/src/components/Attachment/__tests__/Audio.test.tsx +++ b/src/components/Attachment/__tests__/Audio.test.tsx @@ -293,15 +293,14 @@ describe('Audio', () => { it('differentiates between in thread and in channel audio player', async () => { const message = generateMessage(); // A thread player is told apart from a channel player by a Thread instance (useThreadContext), - // not the message-context `threadList` flag, so the thread widget gets a ThreadProvider to - // have its own requester namespace. + // so the thread widget gets a ThreadProvider to have its own requester namespace. render( ({})}> - + diff --git a/src/components/Attachment/__tests__/VoiceRecording.test.tsx b/src/components/Attachment/__tests__/VoiceRecording.test.tsx index c8ef707ca..aacdf43bc 100644 --- a/src/components/Attachment/__tests__/VoiceRecording.test.tsx +++ b/src/components/Attachment/__tests__/VoiceRecording.test.tsx @@ -101,9 +101,9 @@ describe('VoiceRecording', () => { return el; }); const message = generateMessage(); - // A thread player is told apart from a channel player by a Thread instance (useThreadContext), - // not the message-context `threadList` flag. The ThreadProvider around the second widget gives - // it a distinct requester, so two AudioPlayer instances (two `new Audio()`) are created. + // A thread player is told apart from a channel player by a Thread instance (useThreadContext). + // The ThreadProvider around the second widget gives it a distinct requester, so two AudioPlayer + // instances (two `new Audio()`) are created. render( @@ -111,7 +111,7 @@ describe('VoiceRecording', () => { ({})}> - + diff --git a/src/components/Message/Message.tsx b/src/components/Message/Message.tsx index e92d5cfdd..932a0e5b7 100644 --- a/src/components/Message/Message.tsx +++ b/src/components/Message/Message.tsx @@ -185,7 +185,6 @@ export const Message = (props: MessageProps) => { renderText={props.renderText} returnAllReadData={props.returnAllReadData} sortReactions={sortReactions} - threadList={props.threadList} unsafeHTML={props.unsafeHTML} userRoles={userRoles} /> diff --git a/src/components/Message/MessageUI.tsx b/src/components/Message/MessageUI.tsx index 058386a2a..904b7423e 100644 --- a/src/components/Message/MessageUI.tsx +++ b/src/components/Message/MessageUI.tsx @@ -49,9 +49,6 @@ import { extractDisplayInfo as defaultExtractDisplayInfo } from '../Avatar/utils type MessageUIWithContextProps = MessageContextValue; const MessageUIWithContext = ({ - endOfGroup, - firstOfGroup, - groupedByUser, highlighted, isMessageAIGenerated, isMyMessage, @@ -116,7 +113,6 @@ const MessageUIWithContext = ({ const isBounced = isMessageBounced(message); const isEdited = isMessageEdited(message) && !isAIGenerated; - const showMetadata = !groupedByUser || endOfGroup; const showReplyCountButton = !threadInstance && !!message.reply_count; const rootClassName = clsx( @@ -148,9 +144,6 @@ const MessageUIWithContext = ({ 'str-chat__message-send-can-be-retried': message?.status === 'failed' && message?.error?.status !== 403, 'str-chat__message-with-thread-link': showReplyCountButton, - 'str-chat__virtual-message__wrapper--end': endOfGroup, - 'str-chat__virtual-message__wrapper--first': firstOfGroup, - 'str-chat__virtual-message__wrapper--group': groupedByUser, }, ); @@ -229,18 +222,16 @@ const MessageUIWithContext = ({ )} - {showMetadata && ( -
- - {!isMyMessage() && !!message.user && memberCount > 2 && ( - - {message.user.name || message.user.id} - - )} - - {!isDeleted && isEdited && } -
- )} +
+ + {!isMyMessage() && !!message.user && memberCount > 2 && ( + + {message.user.name || message.user.id} + + )} + + {!isDeleted && isEdited && } +
); diff --git a/src/components/Message/__tests__/MessageBubbleContent.test.tsx b/src/components/Message/__tests__/MessageBubbleContent.test.tsx index 978f29c6c..b6a672578 100644 --- a/src/components/Message/__tests__/MessageBubbleContent.test.tsx +++ b/src/components/Message/__tests__/MessageBubbleContent.test.tsx @@ -81,7 +81,7 @@ const renderMessage = async ({ - ({ message, threadList: false })} /> + ({ message })} /> , diff --git a/src/components/Message/__tests__/MessageText.test.tsx b/src/components/Message/__tests__/MessageText.test.tsx index 94f856eb8..61e2d4ff1 100644 --- a/src/components/Message/__tests__/MessageText.test.tsx +++ b/src/components/Message/__tests__/MessageText.test.tsx @@ -64,7 +64,6 @@ const onMentionsClickMock = vi.fn(); const defaultProps = { initialMessage: false, message: generateMessage(), - threadList: false, }; function generateAliceMessage(messageOptions) { diff --git a/src/components/Message/__tests__/MessageUI.test.tsx b/src/components/Message/__tests__/MessageUI.test.tsx index 3e593385c..a7cb566fd 100644 --- a/src/components/Message/__tests__/MessageUI.test.tsx +++ b/src/components/Message/__tests__/MessageUI.test.tsx @@ -133,6 +133,7 @@ describe('', () => { async function renderMessageSimple({ components = {} as Partial, + inThread = false, message, props = {} as Partial, renderer = render, @@ -142,17 +143,16 @@ describe('', () => { channelCapabilities?: Record; channelConfigOverrides?: Record; components?: Partial; + /** Renders the message under a `ThreadProvider`, which is how a message knows it is in a thread. */ + inThread?: boolean; message: ReturnType; props?: Partial; renderer?: typeof render; }) { - // "In a thread" is derived from useThreadContext(), so `props.threadList` wraps the message in - // a ThreadProvider. const messageElement = ( ({ message, - threadList: false, ...props, })} /> @@ -170,7 +170,7 @@ describe('', () => { ...components, }} > - {props.threadList ? ( + {inThread ? ( ({})}> {messageElement} @@ -526,11 +526,11 @@ describe('', () => { it('should not render status when rendered in a thread list and was delivered to other members', async () => { const message = generateAliceMessage(); const { container, queryByTestId } = await renderMessageSimple({ + inThread: true, message, props: { deliveredTo: [alice, bob], readBy: [alice], - threadList: true, }, }); expect(queryByTestId(/message-status/)).not.toBeInTheDocument(); @@ -541,10 +541,10 @@ describe('', () => { it('should not render status when rendered in a thread list and was read by other members', async () => { const message = generateAliceMessage(); const { container, queryByTestId } = await renderMessageSimple({ + inThread: true, message, props: { readBy: [alice, bob, carol], - threadList: true, }, }); expect(queryByTestId(/message-status/)).not.toBeInTheDocument(); @@ -834,6 +834,19 @@ describe('', () => { expect(results).toHaveNoViolations(); }); + // Grouped messages keep their metadata in the DOM; the default CSS hides it on `top` and `middle` + // list items, so an app can show it again with CSS alone. + it.each([['top'], ['middle'], ['bottom'], ['single']] as const)( + 'should render the metadata of a %s group message', + async (groupStyle) => { + const { container } = await renderMessageSimple({ + message: generateAliceMessage(), + props: { groupStyles: [groupStyle] }, + }); + expect(container.querySelector('.str-chat__message-metadata')).toBeInTheDocument(); + }, + ); + // A bounced message has `type: 'error'` and `moderation.action === 'bounce'`. describe.each< [string, Parameters[0] & Record] diff --git a/src/components/Message/types.ts b/src/components/Message/types.ts index aea94b318..cc76530a8 100644 --- a/src/components/Message/types.ts +++ b/src/components/Message/types.ts @@ -33,7 +33,12 @@ export type MessageProps = { // todo: remove /** Overrides the date formatting. Receives the original date and returns a string. */ formatDate?: (date: Date) => string; - /** Styles to apply to this message, e.g. top, bottom, single. */ + /** + * The message's position in its group of consecutive messages from one user: `top`, `middle`, + * `bottom` or `single`. The message lists add it to the list item as `str-chat__li--