chore: land v15 on master - #3304
Conversation
…xt to StateStore instances (#3237) BREAKING CHANGE: requires stream-chat v10. * Removed `ChannelStateContext` and `ChannelActionContext` along with `useChannelStateContext()`, `useChannelActionContext()`, `useCreateChannelStateContext()` and `useCreateChannelActionContext()`. Read the channel via `useChannel()` and message state via `useMessagePaginator()`; both are backed by LLC `StateStore`s. * `ChatContext` no longer exposes the active channel or `setActiveChannel()`. Channel/thread routing is slot-driven — use `useChatViewNavigation()` from `stream-chat-react/slot-layout` (`openChannel`, `openThread`, `closeThread`). * The `TypingContext` provider path was removed from the `Channel` runtime. `TypingIndicator` now derives its state from LLC stores and takes `isMessageListScrolledToBottom` and `scrollToBottom` props. * Context mutation/pagination helpers were replaced by paginator methods: `updateMessage`/`removeMessage` -> `ingestItem`/`removeItem`; `loadMore`/`loadMoreNewer` -> `toTail()`/`toHead()`; `jumpToMessage`/`jumpToFirstUnreadMessage` -> `messagePaginator.jumpToMessage()`/ `jumpToTheFirstUnreadMessage()`. Messages are no longer in `channel.state`. * Custom send/update/delete/markRead overrides move from ChannelActionContext callbacks to `channel.configState.requestHandlers` / `thread.configState.requestHandlers`. * `ChatView` is a navigation landmark, not a WAI-ARIA Tabs widget: the selector is `role="navigation"` with `aria-current` on the active button. There is no `role="tab"`/`"tablist"`/`"tabpanel"`. `activeChatView` renamed to `activeView`. * `ChannelList` renders a `role="listbox"` whose items are `role="option"`. * v10 data shapes: channel name/image read from `channel.data.custom.name`/ `.custom.image`; user custom fields from `user.custom` (e.g. `username`); attachment `duration`/`file_size`/`mime_type`/`waveform_data` from `attachment.custom`. Message moderation uses `message.moderation` (`moderation_details` is gone).
# Conflicts: # examples/tutorial/package.json # examples/vite/package.json # package.json # src/components/Message/MessageRepliesCountButton.tsx # src/components/Threads/ThreadList/ThreadListItemUI.tsx # src/components/TypingIndicator/TypingIndicator.tsx # src/components/TypingIndicator/hooks/useDebouncedTypingActive.ts # src/context/index.ts # src/plugins/ChannelDetail/Views/PinnedMessagesView/PinnedMessagesView.tsx # yarn.lock
BREAKING CHANGE: the <Chat> prop and the ChatContext field channelPaginatorsOrchestrator are renamed to channelManager, and the types ChannelPaginatorsOrchestrator / ChannelPaginatorsOrchestratorState are now ChannelManager / ChannelManagerState. Every useChatContext() consumer reading the old field, and any test that builds a partial ChatContext value, has to be updated. No deprecated prop or type aliases are exported.
…tion keys (#3261) ### 🎯 Goal Closes [REACT-1028](https://linear.app/stream/issue/REACT-1028/i18n-keep-the-english-translations-only). Two problems with i18n in v14, both fixed here. **The 11 non-English locales shipped whether you used them or not.** They were statically imported and copied into `Streami18n` at construction, so they were un-treeshakeable even for an English-only app — ~112 KB gzip (27% of the bundle), plus 11 `dayjs/locale/*` side-effect imports and ~150 lines of calendar config. **Translation keys _were_ the English copy.** `t('Send Message')` meant 375 of 706 `en.json` entries were `"X": "X"` duplication, any copy edit silently orphaned all 12 translations, and the same word in two contexts couldn't be disambiguated — the codebase had already grown an ad-hoc `aria/` prefix to work around exactly that. v15 is the one window where renaming public translation keys is cheap, so both land together. ### 🛠 Implementation details Keys are now stable dotted identifiers, with the English copy inline as i18next's `defaultValue`: ```ts const { t } = useTranslationContext(); t('message.status.sent.text', 'Sent'); t('channel.memberCount.title', { count, defaultValue_one: '{{ count }} member', defaultValue_other: '{{ count }} members', }); ``` Namespaces follow the source tree (`message.*`, `messageComposer.*`, `poll.*`), with shared copy under `common.*` and the modality as the leaf (`.label`, `.ariaLabel`, `.placeholder`, `.title`, `.text`). 633 keys across 724 call sites. Keeping the copy inline is what makes a partial dictionary safe — an unsupplied key still renders English rather than a raw key path — and it means **only 71 keys ship as data** (`timestamp.*`, `duration.*`, `language.*`, and one postProcessor directive: the ones that can't carry an inline default). The other 562 render from the `defaultValue` at their call site, removing a further 39,915 raw bytes of duplicated strings. `src/i18n/keys.ts` is generated from the call sites and is **type-only**, so the typed key surface costs nothing at runtime. `yarn build-translations` regenerates it and hard-fails on a key used with two different copies, a key with no inline default and no bundled entry, or a key defined in both places. CI re-runs it and fails on any diff. --- ## Modifying existing copy Pass overrides for the built-in English. Everything you don't mention is untouched: ```ts import { Streami18n } from 'stream-chat-react'; import type { TranslationDictionary } from 'stream-chat-react'; const i18n = new Streami18n({ translationsForLanguage: { 'textareaComposer.textareaPlaceholder.sendMessage.label': 'Write something…', } satisfies TranslationDictionary, }); <Chat client={client} i18nInstance={i18n}>…</Chat> ``` ``` placeholder → "Write something…" (overridden) common.cancel → "Cancel" (untouched, from the inline default) message timestamp→ "10:30" (untouched, from runtimeDefaults) ``` `registerTranslation('en', {…})` does the same thing after construction. Every language is layered over the bundled defaults — however you select it, and even with no dictionary at all — so overriding one string can't knock out timestamps. ## Catching unknown keys at compile time Key names are checked against the catalog, so a typo or a leftover v14 key is a compile error: ```ts const good: TranslationDictionary = { 'common.cancel.label': 'Dismiss' }; const typo: TranslationDictionary = { 'common.cancel.lable': 'Dismiss' }; // ^ TS2353 — not a key in the catalog const stale: TranslationDictionary = { Cancel: 'Dismiss' }; // ^ TS2353 — v14 key, no longer exists ``` (TypeScript reports the first unknown property per object literal, so fix them one at a time.) No annotation needed — the parameters are typed, so a dictionary written straight into the call is checked too: ```ts i18n.registerTranslation('de', { 'common.cancel.lable': 'Abbrechen' }); // ^ TS2353 — not a key in the catalog ``` This matters because the failure is otherwise silent: a key that doesn't match simply never applies, and you get English with no error. Which type to use: | | plural `_one`/`_other` | extra categories (`_few`, `_many`, `_zero`) | plural suffix on a non-plural key | stale v14 key | your own keys | | --- | --- | --- | --- | --- | --- | | `TranslationDictionary` | ✅ | ✅ | ❌ rejected | ❌ rejected | ❌ rejected | | `LooseTranslationDictionary` | ✅ | ✅ |⚠️ permitted |⚠️ permitted | ✅ | `registerTranslation()` and `translationsForLanguage` take the strict `TranslationDictionary`, so a key written inline is checked — that is where a typo used to slip through. `LooseTranslationDictionary` is the escape hatch for keys the SDK does not define; annotate the variable you pass to opt into it, at the cost of not catching a stale key. Reaching for it is rarely necessary: `TranslationDictionary` accepts any category `Intl.PluralRules` can select, so Russian or Arabic can supply `_few` / `_many` / `_zero` and stay fully checked. `TranslationKey` is the union `t()` accepts; use it to type a `t` parameter. It is **not** the right key type for a dictionary, because plural entries live in the catalog as `_one`/`_other` while `t()` takes the bare handle. Discovering keys: `TranslationKey` autocompletes in any editor, `TranslationCatalog` maps each key to its English copy (hover to see what a key renders, or index it — `TranslationCatalog['common.cancel.label']` is `'Cancel'`), and `yarn i18n:export` writes the 619 translatable keys as JSON for a translator or TMS. The 14 formatter expressions are left out — a TMS that localises `{{value, notification}}` breaks notifications — with `--all` for the full catalog. ## Registering a new language ```ts import { Streami18n } from 'stream-chat-react'; import type { TranslationDictionary } from 'stream-chat-react'; import 'dayjs/locale/de.js'; const de: TranslationDictionary = { 'common.cancel.label': 'Abbrechen', 'channelDetail.channelMembersView.members.title_one': '{{ count }} Mitglied', 'channelDetail.channelMembersView.members.title_other': '{{ count }} Mitglieder', }; const i18n = new Streami18n({ language: 'de', dayjsLocaleConfigForLanguage: { calendar: { sameDay: '[heute um] LT', lastDay: '[gestern um] LT' /* … */ }, }, }); i18n.registerTranslation('de', de); ``` ``` common.cancel.label → "Abbrechen" members.title, n=1 → "1 Mitglied" ← Intl.PluralRules picks the form members.title, n=4 → "4 Mitglieder" common.back.label → "Back" ← not supplied, falls back to English message timestamp → "10:30" ← inherited from runtimeDefaults ``` Plurals are stored as `_one` / `_other`; supply whichever categories your language needs and i18next selects between them, so Russian or Arabic can add `_few`, `_many`, `_zero` — all of them type-checked. Date formats are the one thing you must supply yourself now, and it takes two steps. **1.** Only the `en` dayjs locale is bundled, so import your locale and pass `dayjsLocaleConfigForLanguage` (including its `calendar` block), or pass your own preconfigured `DateTimeParser`. That covers month and weekday names, the plain formats, and the keys that format against the locale's own calendar. **2.** Override the four `timestamp.*` keys that pass their own `calendarFormats`: `DateSeparator`, `ReminderNotification`, `ChannelPreviewTimestamp`, `ChannelDetailPinnedMessageTimestamp`. dayjs takes the calendar wording as part of the format string, so these carry English day words — and a per-key `calendarFormats` replaces the locale's calendar wholesale, so step 1 never reaches them. Skip this and a fully configured German app still renders "Today" in its date separators. [Date and time](ai-docs/i18n-v15-migration.md#date-and-time) has the copy-pasteable German version. --- ### Migration Every v14 key maps to exactly one v15 key. The full 603-row table is [`ai-docs/i18n-v15-key-map.json`](ai-docs/i18n-v15-key-map.json); the integrator-facing guide is [`ai-docs/i18n-v15-migration.md`](ai-docs/i18n-v15-migration.md). Nothing to do if you use the SDK in English and never touched i18n. A later release adding a key will **not** break a custom dictionary — `TranslationDictionary` is `Partial`, so the new string renders its inline English until it is translated. [Keeping a language up to date](ai-docs/i18n-v15-migration.md#keeping-a-language-up-to-date-across-upgrades) shows how to turn that into a compile-time diff of what is still untranslated, and a CI gate for completeness. Also removed: the `deTranslations` … `trTranslations` exports, and the misspelled `geti18Instance()` accessor (use the public `i18nInstance` field). Bundled along the way: **i18next 25 → 26**. None of its breaking changes apply to us — we never used `interpolation.format` (formatters already go through `services.formatter.add`), `initImmediate`, or `i18next.format`. One undocumented v26 change worth knowing: an undefined interpolation value now short-circuits before the formatter runs. No effect on the SDK, but a custom formatter that produced output from an undefined value will stop being called. ### 🎨 UI Changes **None.** Every rendered string is byte-identical — the English copy moved from `en.json` into an inline `defaultValue` at the same call site. The existing suite is the evidence: ~808 assertions on rendered English text, all passing unchanged. <details> <summary>Verification</summary> - `yarn test` — 2789 passed, 227 files - `tsc -p tsconfig.lib.json --noEmit` — clean - `yarn types:scripts` — clean - `yarn lint` — clean - `yarn validate-translations` — clean (regenerates `keys.ts` byte-identically) - `examples/vite` and `examples/tutorial` — `tsc` clean </details>
…3267) ### 🎯 Goal [REACT-1026](https://linear.app/stream/issue/REACT-1026/drop-componentname-param-from-context-consumer-hooks) Context hooks accepted a `componentName` argument used only to name a component in a `console.warn` when the hook was called outside its provider. React DevTools does that better, and the diagnostic was mostly inert: of the 7 hooks accepting it, 3 ignored it and `useTranslationContext` could never reach its warn branch. Meanwhile it was threaded through 138 call sites and often wrong — 11 passed a hook name, several were stale, and 5 passed `Component.name`, which minifies away. BREAKING CHANGE: - **Context hooks no longer accept a `componentName` argument.** `useChatContext('X')` and friends stop type-checking; drop the argument. - **Required contexts throw when used outside their provider**, instead of logging a warning and returning `{}`. Affects the 14 hooks listed above. Components must be rendered within the provider they read from — e.g. anything calling `useChatContext` needs a `<Chat>` ancestor. - **`useChannelInstanceContext` returns `Partial<ChannelInstanceContextValue>`** — `channel` is now typed as possibly `undefined`. - **`ChatViewContext` has no default value** (typed `| undefined`), so `useContext(ChatViewContext)` may return `undefined`. - **`MessageComposerContext` is typed as `MessageComposerContextValue | undefined`**, correcting a narrower declaration that was papered over with casts. - **The gallery header no longer uses the `ComponentContext.MessageTimestamp` override**; it renders the item's own timestamp. ### 🛠 Implementation details Parameter removed from all 7 hooks and their 138 call sites. Missing-provider behaviour also normalized, since `src/` had three competing conventions (warn + `{} as T`, throw, silent cast). The `{} as T` fallback deferred failures to a downstream `Cannot read properties of undefined` several frames away. Everything now goes through `requireContext` (`src/context/requireContext.ts`): - **Required → throw**, naming hook and provider: `useChatContext`, `useMessageContext`, `useMessageComposerContext`, `useMessageListContext`, `useVirtualizedMessageListContext`, `useMessageBounceContext`, `usePollContext`, `useDialogManager`, `useSearchContext`, `useSearchSourceResultsContext`, `useContextMenuContext`, `useChannelListItemContext`, `useGalleryContext`, `useChatViewContext` - **Optional by design → keep default, honest type**: `useTranslationContext`, `useComponentContext`, `useChannelInstanceContext` (now `Partial`), `useModalContext`, `useAriaLiveAnnouncer`, `useMessageTranslationViewContext` - **Renders both inside and outside a provider → read the raw context**: `Audio`, `VoiceRecording`, `CardAudio`, `Timestamp`, `MessageRepliesCountButton`, `MessageComposerUI`, `ModalGallery` Two changes were needed before their hooks could throw: - `WithDragAndDropUpload` detected the composer via `Object.keys(ctx).length > 0`; now uses a `useIsWithinMessageComposerContext()` predicate. Adds the test file this component lacked. - `GalleryHeader` read message context while rendering outside any `MessageProvider`. `GalleryItem` now carries `user` and `createdAt` — wrapping the call sites would be wrong, since `ChannelMediaView` flattens many messages into one list. Adds a `timestamp.GalleryTimestamp` key. `ChatViewContext` also loses its module-level `LayoutController` default, which let unrelated subtrees write to one shared instance. `src/context/__tests__/missingProviderContract.test.tsx` pins the required/optional split. ### 🎨 UI Changes The channel-media gallery header now shows a per-item sender and timestamp, which it previously couldn't display at all. No screenshots: the demo users on the shared environment have no channels, and seeding one would write test data others would see. Covered by unit tests in `GalleryUI.test.tsx` — worth a manual look.
…#3268) ### Goal [REACT-1027](https://linear.app/stream/issue/REACT-1027/remove-the-deprecated-message-component-override-from-componentcontext) `ComponentContext.Message` was deprecated in favour of `MessageUI` during v14 but never removed. Removing it surfaced four more ways to reach the same component — a `Message` prop on `Message`, `MessageList`, `VirtualizedMessageList` and `Thread` — now all collapsed onto the `MessageUI` slot. BREAKING CHANGES: - **`ComponentContext.Message` removed** → use the `MessageUI` slot. - **The `Message` prop is removed from `Message`, `MessageList`, `VirtualizedMessageList` and `Thread`**, and therefore from `additionalMessageListProps`, `additionalVirtualizedMessageListProps` and `additionalParentMessageProps`. To replace a per-component prop, scope the slot to that subtree: ```tsx // before <Thread Message={CustomThreadMessage} /> // after <WithComponents overrides={{ MessageUI: CustomThreadMessage }}> <Thread /> </WithComponents> ``` - **`areMessagePropsEqual` no longer compares the message UI component** — it comes from context, and context updates re-render consumers regardless of `React.memo`. `VirtualMessage` is unchanged and still wins inside `VirtualizedMessageList`, but now only within that list. Full migration detail: `ai-docs/ai-migration-v14-v15.md`. ### Implementation details `MessageProps.Message` was the transport `VirtualizedMessageList` and `Thread` used to inject their resolved component, so deleting it naively would have silently broken the `VirtualMessage` slot. Instead, `VirtualizedMessageList` applies `VirtualMessage` to its own subtree's `ComponentContext` — wrapping only when the slot is set — which also retires `VirtuosoContext.Message`. `Thread`'s three-step resolution collapses entirely; context already carries the component. `WithComponents` now memoizes its merged override map: required, not cosmetic, since an unmemoized provider on the list's render path would defeat per-message memoization. `VirtualMessage` had no test coverage, which is what made this risky. Added three tests — precedence, subtree scoping, unset fallback — and the precedence one fails on `release-v15`. They assert what `useComponentContext()` resolves to rather than inspecting rendered items, because Virtuoso renders zero items under jsdom and an item-level assertion would pass either way. Verified: `tsc -p tsconfig.lib.json --noEmit` clean · `yarn test` 231 files, 2828 passed / 1 skipped · `yarn lint-fix` clean. Each documented failure mode was compiled to confirm it errors (`TS2322` for props, `TS2561` for override keys). **Unrelated change:** the v14 → v15 guide also gains a section on the context-hook `componentName` removal from #3267, included at the author's request rather than split out. ### UI Changes None — override plumbing only; the default message UI and every existing `MessageUI` / `VirtualMessage` override render exactly as before.
…3269) ### Goal The ESM build collapsed the whole source tree into a single chunk, which defeats tree-shaking in consumer apps: importing only `Chat` pulled 405kB. ### Implementation details - `preserveModules` on the ESM output only, so `dist/es` mirrors `src` one file per module (11 files -> 516). Consumer bundlers drop unused modules first, guided by our `sideEffects` field, and only then attempt statement-level elimination -- a merged chunk leaves nothing to drop. CJS stays chunked, since consumers do not tree-shake CJS. - Entry filenames and the `package.json` exports map are unchanged. - Measured consumer bundles: `import { Chat }` 405.1 -> 33.1 kB, `Chat + Channel + MessageList` 451.7 -> 332.6 kB, `stream-chat-react/slot-layout` 426.1 -> 160.4 kB. Whole-SDK import unchanged. Published package grows ~199kB raw. - Dropped the `browserslist` field: nothing in the repo reads it and its query contradicted the ES2022 output floor. Fixed the stale ES2020 target note in CLAUDE.md and recorded why `emptyOutDir` must stay `false`. Verified: `yarn build` green, 231 test files / 2825 tests pass, ESM and CJS entries both load with 611 exports and a single shared `ChatContext`. ### UI Changes None, build output only.
### 🎯 Goal Relevant stream-chat-js PR: GetStream/stream-chat-js#1832 ### 🛠 Implementation details _Provide a description of the implementation_ ### 🎨 UI Changes _Add relevant screenshots_
…3271) Adopts the shared i18n layer from `stream-chat/i18n`, deleting ~1,100 lines of runtime this package no longer needs to own: `Streami18n`, the formatter/date half of `i18n/utils.ts`, `TranslationBuilder/TranslationBuilder.ts`, `externalStrings.ts`, and most of the codegen script (249 lines → ~40, over the generator core now ships). What stays here is what is genuinely this SDK's: the generated key catalog, `runtimeDefaults`, and the notification translation topic. **Behaviour changes** - Notification copy is a `Record<CoreNotificationType, Translator>`, so a new identifier in `stream-chat` is a compile error until mapped. Dead rows for identifiers this SDK never emits are gone; three previously-unmapped core identifiers now translate instead of rendering untranslated English. - The 57 `language.*` entries move to core, generated from `TranslationLanguage`, so `MessageTranslationIndicator` drops its `asDynamicKey` + string-compare miss detection. - Reactivity is core's `StateStore`. `setLanguage()` returns `void`; `getTranslators()` is now `init()`. **No deprecated aliases** — `Streami18n` keeps its name, so integrator code is unchanged there. - Two timestamp edge cases render differently; both documented in `ai-docs/i18n-v15-migration.md`. - Drops `i18next` / `dayjs` / `moment-timezone` from `dependencies` — core supplies the first two, and the third's type leak into the published `.d.ts` is replaced by core's structural `DateTimeLike`. Also adds a `catalogRenders` test (this package had no equivalent of RN's regression net) and puts `release-v15` in `size.yml`'s branch filter, which was only running on `master`. **Verified** against a locally packed core: 2,829 tests, `validate-esm`, `validate-cjs`, lint. Adopting the shared layer surfaced seven real defects in it, all fixed in the core PR with regression tests.⚠️ **Blocked:** needs `stream-chat@10.0.0-rc.3` (GetStream/stream-chat-js#1830). The lockfile is deliberately untouched and must be regenerated once that publishes — until then `yarn install --immutable` fails, hence draft. Pre-existing and not from this PR: `yarn build`'s `tsc` step fails on 3 imports (`APIErrorResponse`, `EventAPIResponse`) removed from core after rc.2. Needs fixing when this package bumps its core range.
Not yet ready to be merged Relevant stream-chat-js PR: GetStream/stream-chat-js#1836 ### 🎯 Goal _Describe why we are making this change_ ### 🛠 Implementation details _Provide a description of the implementation_ ### 🎨 UI Changes _Add relevant screenshots_ --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
BREAKING CHANGE: request-handler props are removed from `Channel` and
`Thread`.
`doSendMessageRequest`, `doUpdateMessageRequest`,
`doDeleteMessageRequest` and
`doMarkReadRequest` are gone, along with `useChannelRequestHandlers`,
`useThreadRequestHandlers` and `useEditMessageHandler`. Register once on
the client:
```
// v14
<Channel channel={channel} doSendMessageRequest={mySend}>
// v15
client.config.set({
channel: {
requestHandlers: {
sendMessageRequest: async ({ localMessage, message, options }) => ({
message: await mySend(message, options),
}),
},
},
});
```
Three differences: handlers take a single params object and return `{
message }`; thread
flows register under the `thread` key; and registration is per CLIENT,
not per mounted
subtree — the one thing the props could do that this cannot. Per-channel
behaviour now
needs a branch inside one handler on the `cid` it receives.
BREAKING CHANGE: `useChannelConfig` returns the channel's RESOLVED
configuration instead
of the channel type's raw server config, so field names change:
channelConfig?.typing_events → channelConfig?.typingEvents.enabled
channelConfig?.read_events → channelConfig?.readEvents.enabled
channelConfig?.replies → channelConfig?.replies.enabled
channelConfig?.user_message_reminders →
channelConfig?.userMessageReminders.enabled
channelConfig?.commands → channelConfig?.availableCommands
Every gate is now the server flag ANDed with what is registered through
`client.config`,
which is what the client actually enforces. The hook also subscribes:
these were getters,
so a `client.config.set()` previously never reached the screen. No
component reads
`channel.serverConfig` any more.
BREAKING CHANGE: `useAttachmentManagerState` replaces
`hasCustomDoUploadRequest` with
`customCdn`, and gains `attachmentsEnabled`, `locationEnabled` and
`pollsEnabled`.
BREAKING CHANGE: the per-domain stores on `channel.state` are removed —
`readStore`,
`typingStore`, `membersStore`, `watcherStore`, `ownCapabilitiesStore`,
`mutedUsersStore`.
Subscribe to `channel.state` itself; the selector is unchanged, because
`ChannelStateData`
is flat and keeps the same top-level keys:
useStateStore(channel.state.ownCapabilitiesStore, sel) →
useStateStore(channel.state, sel)
`WatcherState` is renamed `ChannelWatchState`, and
`channel.disconnected` is renamed
`channel.pendingDisposal` (removed outright — no deprecated alias).
BREAKING CHANGE: `AIStates` is no longer exported from
`stream-chat-react` — import it
from `stream-chat`. The values are unchanged, but it is now
literal-typed, so an inferred
array of its members no longer accepts the wide `AIState`:
const STOPPABLE: readonly AIState[] = [AIStates.Thinking,
AIStates.Generating];
BREAKING CHANGE: `<Channel>` marks its channel active while mounted and
reloads it on
`connection.recovered`. The client skips re-seeding an active channel's
message list on
hydration and reconnect, so the consumer owns that window. A custom
channel surface that
does not render `<Channel>` must call `channel.activate()` /
`deactivate()` and
`channel.reload()` itself, or its list goes stale after a reconnect.
Relevant stream-chat-js PR: GetStream/stream-chat-js#1849 ## Breaking changes - `MessageListProps.headerPosition` is now nanoseconds - `ChatContextValue. latestMessageDatesByChannels` record now stores nanoseconds instead of Dates - `VirtualizedMessageListProps. lastReadDate` is now nanoseconds instead of Date - `ProcessMessagesContext.lastRead` is now nanoseconds instead of Date - `ProcessMessagesParams.lastRead` is now nanoseconds instead of Date The change can produce runtime errors not caught by TS compiler; integrators should check their code for these potential issues: ``` # the idioms that now fail silently grep -rn "isDate(\|instanceof Date\|\.getTime?\?\.\?(\|as string | Date" src # raw timestamps interpolated into translations grep -rn "t(.*timestamp\|t(.*date" src # every surviving `new Date(x)` with an argument grep -rn "new Date([^)]" src ``` --------- Co-authored-by: Oliver Lazoroski <oliver.lazoroski@gmail.com>
> Tracked in [REACT-1172](https://linear.app/stream/issue/REACT-1172/extract-the-i18n-foundation-into-stream-ioi18n) Retargets every `stream-chat/i18n` import to [`@stream-io/i18n`](GetStream/js-toolkit#51), where the shared runtime now lives. The API is unchanged — only the specifier moves. > **BREAKING** — requires `@stream-io/i18n` and `stream-chat@^10.0.0-rc.10`. ##⚠️ CI will fail the install step until the dependencies publish `@stream-io/i18n@1.0.0` and `stream-chat@10.0.0-rc.10` are not on npm yet, so `yarn.lock` **cannot be regenerated** and is deliberately left untouched in this PR. Everything else is reviewable now. Locally this was verified against packed tarballs of both. ## What changed - Every `stream-chat/i18n` import → `@stream-io/i18n` (5 files plus the codegen script). - `languageNameDefaults` / `LanguageNameCatalog` now come from `stream-chat`'s **root** barrel — they enumerate the languages the Chat API can auto-translate a message into, so they stayed with Chat. - `TranslationContext` and `useStreami18n` are built on the shared bindings in `@stream-io/i18n/react`, so this SDK and React Native stop carrying two copies of the same context plumbing and store subscription. What stays this SDK's own: the catalog-typed `t`, the notification translation topic, and supplying a context **default** rather than throwing — which is what lets a primitive render outside `<Chat>` (React Native throws instead, which is why the shared factory takes that as an option). `@stream-io/i18n` is a **regular dependency, not a peer**: an integrator never imports it — they import the `Streami18n` this package subclasses and re-exports. ## Also fixes: `yarn types` checked nothing It ran `tsc` with no `--project`, so it picked up the root solution file (`"files": []`) and exited 0 **even with a deliberate type error in `src/`**. It is now `tsc -p tsconfig.lib.json --noEmit`, verified to fail on a planted error, and added to CI — which previously typechecked only the build scripts, so a library type error could reach `master`. `CLAUDE.md` / `AGENTS.md` updated accordingly (they documented the trap rather than fixing it). ## Verification `yarn types` (the real one), `yarn lint`, and **2,769 tests** all pass against locally-linked builds of `@stream-io/i18n` and `stream-chat`. Also verified end-to-end in the Vite example: switching language at runtime updates copy, plurals, placeholders, aria-labels **and** dates — `Tue, 2 Aug` → `Di., 2. Aug.`, `08/02/2022` → `02.08.2022` — with no reload. ## Merge order After [js-toolkit#51](GetStream/js-toolkit#51) and [stream-chat-js#1861](GetStream/stream-chat-js#1861), then rerun `yarn install` to commit a real lockfile.
…ow its responsibilities (#3286) BREAKING CHANGE: `ChannelProps.channel` is now required and `EmptyPlaceholder` is removed. Render the exported `ChannelPlaceholder` to fill the same `.str-chat__channel` layout slot while no channel is selected. BREAKING CHANGE: `Channel` no longer queries the channel; `initializeOnMount` and `channelQueryOptions` are removed. Whoever supplies the channel initializes it — use the exported `getChannel({ channel, client })`, which watches and de-duplicates concurrent calls. Channels from `ChannelList` or any `queryChannels` call arrive watched already. `Channel` no longer renders `LoadingIndicator` or `LoadingErrorIndicator` for a query it does not make, and an uninitialized channel renders as an empty channel with no error. BREAKING CHANGE: `Channel` no longer writes `document.title`, and `activeUnreadHandler` is removed. See `examples/vite/src/DocumentTitleManager` for a working replacement in application code. BREAKING CHANGE: the `channel.channelMissing.text` translation key is removed. This is a compile error (TS2353) for any dictionary typed with the exact `TranslationDictionary` — delete the entry from custom locale files. BREAKING CHANGE: `ChatContext.latestMessageDatesByChannels` is removed. Slow-mode cooldown now lives on `channel.cooldownTimer` (`useCooldownRemaining` / `useIsCooldownActive`), and the channel's latest message on `channel.messagePaginator.aggregateState.lastMessage`. BREAKING CHANGE: `Channel` no longer jumps to a message focused from search. Selecting a search result calls `channel.messagePaginator.jumpToMessage(id)` directly, and the highlight is read from `channel.messagePaginator.messageFocusSignal`. A custom message list that relied on `Channel` performing the jump must call `jumpToMessage` itself. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ole app (#3287) BREAKING CHANGE: `ChannelProps.allowConcurrentAudioPlayback`, `ThreadProps.allowConcurrentAudioPlayback` and `WithAudioPlaybackProps.allowConcurrentPlayback` are removed, with no replacement. If you passed `true`, delete it: starting a player now pauses whichever was playing and takes the shared element over. If you passed `false` or nothing, the only change is that playback is exclusive across surfaces rather than within one. BREAKING CHANGE: `useActiveAudioPlayer()` now reports the app-wide active player rather than the calling surface's.
…sage lists (#3288) BREAKING CHANGE: the `messageActions` prop is removed from `Channel`, `MessageList`, `VirtualizedMessageList`, `Thread` and `Message`. Filter `defaultMessageActionSet` and pass the result as `messageActionSet` instead. BREAKING CHANGE: `MESSAGE_ACTIONS`, `OPTIONAL_MESSAGE_ACTIONS`, `getMessageActions()`, and the `MessageActionsArray` and `Capabilities` types are no longer exported. Use the new `DefaultMessageActionType` union for the action types the SDK ships. BREAKING CHANGE: `MessageContext` no longer carries `getMessageActions` or `actionsEnabled`. The rendered action set is decided by `messageActionSet` and each item's own `isVisible` predicate. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ren (#3289) BREAKING CHANGE: `Thread` requires a `thread` prop and renders `children`. `additionalMessageComposerProps`, `additionalMessageListProps`, `additionalParentMessageProps`, `additionalVirtualizedMessageListProps`, `autoFocus` and `virtualized` are removed — pass those props to the children you render. `Thread` provides the thread itself, so it no longer needs to be wrapped in `ThreadProvider`. The `str-chat__thread--virtualized` class is gone; `Thread` cannot know which list you chose. BREAKING CHANGE: `Thread.enableDateSeparator` is removed with nothing in its place. A `MessageList` in a thread now follows its own default and shows date separators unless you pass `withDateSeparator={false}`. BREAKING CHANGE: `disableDateSeparator` is renamed to `withDateSeparator` on `MessageList` (default `true`, was `disableDateSeparator={false}`) and `VirtualizedMessageList` (default `false`, was `disableDateSeparator={true}`). The rename also reaches `processMessages`, `useEnrichedMessages`, `FloatingDateSeparator`, `useFloatingDateSeparator` and `useFloatingDateSeparatorMessageList`. BREAKING CHANGE: the `head` prop is removed from `MessageList` and `VirtualizedMessageList`. Both render the thread's parent message from context; override it with a `ThreadHead` component on `ComponentContext`. BREAKING CHANGE: `ThreadHeaderProps` is down to `overrideTitle`. `thread` comes from the thread in context, and `closeThread` goes through workspace navigation — customize it centrally via `ChatView`'s `deriveWorkspaceNavigation`, or call the new `useCloseThread()` hook from a header of your own. `ComponentContext.ThreadHeader` is removed; compose the header you want. BREAKING CHANGE: `ThreadSlot` wraps its children in `<Thread>` instead of letting them replace the whole panel, mirroring `ChannelSlot`. Markup that must sit outside the thread container moves outside `ThreadSlot`; anything needing thread context must sit inside. BREAKING CHANGE: `Thread` no longer rebuilds its subtree when the thread changes. A custom component rendered inside `Thread` that relied on being remounted per thread — to reset local state, or to re-run a mount effect — must depend on the thread instead. The SDK's own message list does this by keying on the thread; `WithAudioPlayback` by taking `playbackScope`. BREAKING CHANGE: `LegacyThreadContext` and its `legacyThread` value are removed. The thread composer resolves its composition context from the `Thread` instance in `ThreadContext`.
…ches the live head (#3290) Jumping to a search result deep in history and then scrolling down teleported the user to the newest message. Scrolling closes the gap page by page, and when the last one lands the loaded window merges into the live head -- `hasMoreNewer` turns false. Two autoscroll paths read that as "we are at the head now, pin to bottom": the layout effect that keeps `hasMoreNewer` in its dependencies so a list mounting before its first page still autoscrolls, and the scroll manager's append branch, which sees the merged page as messages added below. Neither is right here. The user is reading where they jumped to, and the merged page lands below them -- nothing about that transition asks for the viewport to move. `useScrollLocationLogic` now derives `justReachedLatestMessageSet` from the `hasMoreNewer` transition itself and suppresses both paths for that one render. A trailing layout effect advances the ref afterwards, declared after the autoscroll effects and the scroll manager's own so all three observe the transition before it is consumed. `useMessageListScrollManager` has accepted this flag since #3027 and returns early on it, but nothing ever passed it -- that branch was dead until now.
BREAKING CHANGE: the `stream-chat` peer range moves to `^10.0.0-rc.12`. That release removes `connection.changed`, changes `client.wsConnection` from the socket itself to a wrapper, and moves the socket's settings into configuration. See GetStream/stream-chat-js PR 1859 for the full list; an application on an earlier `stream-chat` will not typecheck against this version. BREAKING CHANGE: connectivity is read from state stores rather than events. Code that listened for `connection.changed` — to render its own banner, to refetch on reconnect — must subscribe to `client.wsConnection.state` or `client.networkConnection.state` instead, or use the new `useWSConnectionState` / `useNetworkConnectionState` hooks. Nothing in this package dispatches or forwards the old event. Note the two stores name their fields differently: the socket's `isHealthy` is always a boolean, while the network's `isOnline` is `boolean | undefined`, where `undefined` means no platform reporter has reported yet and must be tested with `=== false`. BREAKING CHANGE: `Channel` no longer reloads its channel on `connection.recovered`. The client reloads every active channel itself and dispatches the event afterwards, so handling it here reloaded each open channel twice per reconnect. Anything relying on the component to refresh a channel after a reconnect should rely on the client instead. BREAKING CHANGE: a socket drop on a working network now renders "Reconnecting…" rather than "Waiting for network…", from the new `chat.reportLostConnection.reconnecting.text` key. Tests and dictionaries keyed on the old copy for that case need updating; the `waitingNetwork` key is unchanged and still used when the device network is down. The delay before either banner appears now comes from `client.wsConnection.config.offlineNotificationDisplayDelayMs` (5s default) rather than from a fixed hold inside the client, so changing that config changes when the banner shows. The `system:network:connection:lost` notification type is unchanged.
…3295) BREAKING CHANGE: `useSendMessageFn` and `useUpdateMessageFn` are removed. Call `messageComposer.send()` and `messageComposer.update()` instead. They resolve `'sent' | 'nothing-to-send' | 'failed'` rather than a boolean, so a truthiness check on the result now passes for every outcome — compare against `'sent'`. BREAKING CHANGE: the two message submission failure notifications changed key. `messageComposer.sendMessageFn.sendMessageRequestFailed.text` is now `notification.messageSendFailed`, and `messageComposer.updateMessageFn.editMessageRequestFailed.text` is now `notification.messageUpdateFailed`. A typed custom translation dictionary fails to compile until both are renamed. BREAKING CHANGE: `RetryHandler` takes `Omit<OperationParams<'retry'>, 'message'>` instead of `RetrySendMessageWithLocalUpdateParams`, which stream-chat removed in GetStream/stream-chat-js#1882. Code that names the old type must be updated.
### 🎯 Goal Adopt stream-chat's branded `TimestampNS` (GetStream/stream-chat-js#1884, [REACT-1179](https://linear.app/stream/issue/REACT-1179)). stream-chat now types every server-sent date as `TimestampNS`, a branded unix-nanosecond `number`. Its published types also make `new Date(timestamp)` a compile error, because a nanosecond value is out of `Date`'s range and silently becomes an Invalid Date. An audit of this SDK found **no runtime unit bugs**: every server timestamp that becomes a `Date`, gets formatted, or meets `Date.now()` already goes through `convertTimestampToDate` / `nsToDate` / `nsToMs`. The work is closing the type gaps so the brand, and with it the guard, reaches the public props and the tests. > **Requires `stream-chat@^10.0.0-rc.14`**, the first release with GetStream/stream-chat-js#1884 (`TimestampNS`, `asTimestampNS`, the `DateConstructor` guard); this PR bumps the dependency. ### 🛠 Implementation details **Public types (breaking)** These now take `TimestampNS`, so a millisecond `number` no longer compiles: - `ProcessMessagesParams.lastRead` (`processMessages`) - `MessageList` `headerPosition` / `insertIntro`. It already had to be in nanoseconds, but was typed `number`. - the `VirtualizedMessageList` `lastReadDate` render prop - `ChannelFilesView.utils`'s internal `normalizeTimestamp` **Tests and fixtures** - `mock-builders/generator/time.ts` `convertDateToTimestamp` returns `TimestampNS`, which makes the generated fixtures assignable (about 50 test errors fixed in one place). - The reminder, reaction and delivered-event builders brand their values. - Hand-written literals (epoch `0`, `NaN`, `now - msToNs(…)`) go through `asTimestampNS`. - `MessageList/__tests__/utils.test.ts` passed a `Date` as `lastRead` through an untyped helper; it now passes `nowNs()`, and the helper is typed. - `NotificationAnnouncer.test.tsx` imported a type from the sibling `../stream-chat-js/src` checkout; it now imports from `stream-chat`. That removes 17 errors from `yarn types:tests`. **Other** - The Vite example's WebSocket event templates type their timestamps `TimestampNS`. - `ai-docs/ai-migration-v14-v15.md` now documents: - the brand and the Date guard; - what the guard does not catch: date libraries, `ts ?? Date.now()`, `Math.max(…)`, and unit mix-ups; - `asTimestampNS` and the updated type table. - The same doc previously listed `latestMessageDatesByChannels` as retyped; it now points to the section that records its removal. `i18n-v15-migration.md` no longer destructures it from `useChat`. - A stale comment on `ChatContextValue.mutes` is fixed. **Verification** (against the published `stream-chat@10.0.0-rc.14`, after merging the latest `release-v15`) - `yarn types`: clean. The unrelated errors this PR described earlier (`RetrySendMessageWithLocalUpdateParams`, the notification translators) were fixed on `release-v15` by #3295 / #3296. - `yarn test`: 2840 passing, 252 files. The `Channel.test.tsx` failure is gone too. - `yarn lint` and `yarn validate-translations`: clean. - I ran the Vite example. Channel list, message list, date separators, search results and a simulated `message.new` all render correct dates, with no "Invalid Date" and no console errors. ### 🎨 UI Changes None. Types, tests and docs only.
BREAKING CHANGE: `ChannelListItemProps.channelUpdateCount` is removed. The unread badge now updates without it. BREAKING CHANGE: `ChannelListItemProps.key` is removed. React's own `key` prop is unaffected. BREAKING CHANGE: `ChannelListItemProps.watchers` is removed. It had no effect in v15.
…mposer edits (#3299) BREAKING CHANGE: the `preventClearingOnUnmount` prop is removed from `MessageComposer`. Drop it. If you relied on unmounting to clear a composer supplied through `MessageComposerControllerProvider`, call `clear()` on it yourself when your UI is done with it.
…orkspace navigation (#3301)
Merge `release-v15` into `master`, so that `master` carries the v15 line and publishes it as release candidates, while `release-v14` carries v14. The merged tree is `release-v15`'s, except for: - `CHANGELOG.md`, which keeps `master`'s entries for 14.11.0 to 14.12.0; - `.releaserc.json`, which publishes `release-v14` to npm `latest` and `master` to npm `rc`, and analyzes commits with the `conventionalcommits` preset so a `!` marks a breaking change, plus a `breaking` rule so breaking refactors are not held to a patch; - `AGENTS.md`, `developers/BRANCHES.md` and `size.yml`, which name `master` and `release-v14` instead of `release-v15`. Every v14 change since the fork was ported to `release-v15` as its own commit, so every conflict resolves to `release-v15`'s version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Review skippedToo many files! This PR contains 783 files, which is 633 over the limit of 150. To get a review, reduce the PR to 150 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: ⛔ Files ignored due to path filters (8)
📒 Files selected for processing (783)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Size Change: -244 kB (-22.26%) 🎉 Total Size: 850 kB 📦 View Changed
ℹ️ View Unchanged
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3304 +/- ##
==========================================
+ Coverage 85.51% 85.68% +0.17%
==========================================
Files 512 532 +20
Lines 16195 15657 -538
Branches 5127 4943 -184
==========================================
- Hits 13849 13416 -433
+ Misses 2346 2241 -105 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
🎉 This PR is included in version 15.0.0-rc.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Land this with a fast-forward push, not through GitHub:
release-v15's files.The push adds exactly one merge commit to
master, and GitHub then marks this PR as merged.masteris frozen until then. If it moves, this merge has to be redone.🎯 Goal
Land v15 on
master:masterbecomes the v15 line and publishes release candidates (15.0.0-rc.N) to npmrc.release-v14is the stable v14 line and publishes to npmlatest. It was set up in chore(release): make release-v14 the stable v14 line #3303, and its dry run found no new version.The full
release-v15history comes along: 35 commits, with their original hashes, authors and messages. After this lands,release-v15can be retired.🛠 Implementation details
One merge commit (
9b7c9e0ae), with parentsmaster(0fb5ac306, 14.12.0) andrelease-v15(a6ed6aa35).Every v14 change since the fork at 14.10.0 was ported to
release-v15as its own commit (REACT-1180), or ruled out (#3257). So all 170 conflicts resolve torelease-v15's version, and the merged files arerelease-v15's except for five:CHANGELOG.mdmaster's 14.11.0, 14.11.1 and 14.12.0 entries, whichrelease-v15's changelog lacks.releaserc.jsonrelease-v14:release-v14→latest,master→rcprerelease. The version-picking plugin switches fromangulartoconventionalcommits, so!marks a breaking change (as in GetStream/stream-chat-js#1837). It also gets a{ "breaking": true, "release": "major" }ruleAGENTS.mdmasterand published as release candidates; v14 fixes go torelease-v14developers/BRANCHES.mdrelease-v14.github/workflows/size.ymlrelease-v15→release-v14Why the extra
breakingrule. This repo has a custom rule "refactor→ patch". Underconventionalcommits, arefactor!:commit is parsed as typerefactor, that custom rule matches, and semantic-release never checks whether the commit is breaking. Without the extra rule, #3289, #3288 and #3016 would drop from major to patch. When several rules match, the highest wins.Replay of both presets through the real version-picking plugin, over 432 commits (the last 400 on
masterplus the 35 from v15):!-only commits: feat!: port the tutorial alignment (#3251) to v15, with panel-aware workspace navigation #3301, feat!: adopt stream-chat's branded TimestampNS #3297, refactor(i18n)!: consume the shared runtime from @stream-io/i18n #3284, feat!: move to timestamps in response models #3278, refactor(i18n)!: adopt the shared i18n layer from stream-chat/i18n #3271, plus two v14 betas that come before the v14.12.0 tag.release-v14keepsangularon purpose. It publishes tolatestand can't be capped at 14.x, so a strayfeat!:there is ignored instead of publishing 15.0.0.✅ Verification
A local dry run of semantic-release 25.0.3 on this exact merge proposes
15.0.0-rc.1on thercchannel: it found v14.12.0, 36 new commits, and a major release.After it lands
masterredeploys the vite example to Vercel production, for both the Development and Public projects. Those sites then run v15.masterin dry-run mode, then for real. That publishes15.0.0-rc.1torcand commits itsCHANGELOG.mdentry.BREAKING CHANGE:footer. Tidy the GitHub release and theCHANGELOG.mdentry afterwards.release-v15, which has no open PRs, and unfreezemaster.🎨 UI Changes
None beyond v15 itself.