Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
48 changes: 46 additions & 2 deletions ai-docs/ai-migration-v14-v15.md
Original file line number Diff line number Diff line change
Expand Up @@ -2089,7 +2089,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<MessageProps>`). 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`

Expand Down Expand Up @@ -2121,6 +2122,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 `undefined` for the others, so it computes wrong styles or throws, for example on `previousMessage.created_at`; TypeScript rejects it.

```tsx
// v14
<MessageList noGroupByUser />
<VirtualizedMessageList shouldGroupByUser />
<MessageList
groupStyles={(message, previousMessage, nextMessage, noGroupByUser, maxTime) =>
'pinned' in message && message.pinned
? 'single'
: getGroupStyles(message, previousMessage, nextMessage, noGroupByUser, maxTime)
}
/>

// v15
<MessageList groupByUser={false} />
<VirtualizedMessageList groupByUser />
<MessageList
getGroupStyles={(params) =>
'pinned' in params.message && params.message.pinned ? 'single' : getGroupStyles(params)
}
/>
```

### Custom `renderMessages` gets different options

- `RenderMessagesOptions.channel: Channel` is new and required.
Expand All @@ -2132,6 +2168,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
Expand Down Expand Up @@ -2234,6 +2271,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)).
Expand Down Expand Up @@ -3073,6 +3114,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 `<ChannelList>` has no `.str-chat__channel-list` ancestor. |
| The channel list root carried `str-chat <theme> str-chat__channel-list` and `customClasses.channelList` | Inside `ChatView` the `str-chat <theme>` 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 <theme> str-chat__channel-list`, and a bare `<ChannelList>` wraps itself in `div.str-chat.<theme>.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 |
Expand All @@ -3082,7 +3124,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 .
```
Expand Down Expand Up @@ -3257,3 +3299,5 @@ Run after every section is applied.
| BC-082 | Custom preview slots mount only while their item is visible | [13](#new-composer-previews-are-a-configurable-set) |
| BC-083 | Message action handlers reject | [12](#message-action-handlers-reject-when-they-fail) |
| BC-084 | First message loses its top padding | [19](#19-css-and-markup) |
| BC-085 | `MessageContext` grouping flags and `virtual-message__wrapper--*` classes | [12](#messagecontext-grouping-flags-are-removed-read-groupstyles), [19](#19-css-and-markup) |
| BC-086 | `groupByUser`, `getGroupStyles` | [11](#message-grouping-groupbyuser-and-getgroupstyles) |
Loading
Loading