feat: migrate components from ChannelActionContext and ChannelState context to dedicated StateStore instances and add layout control API#3237
Draft
MartinCupela wants to merge 890 commits into
Conversation
## 🎯 Goal Add dark theme support to `stream-chat-react` and wire the Vite example so the SDK can be exercised in both light and dark mode. Linear ticket: https://linear.app/stream/issue/REACT-798/add-support-for-dark-theme ## 🛠 Implementation details - add dark semantic token overrides for the React SDK and scope them to `str-chat__theme-dark` - make `Chat`/portal rendering respect the dark theme class and update the Vite example with a theme toggle - align loading shimmers and image-loading overlays with the theme-aware skeleton tokens ## 🎨 UI Changes https://github.com/user-attachments/assets/5e05301c-47c7-421f-8d1a-5af4eb778ef5 --------- Co-authored-by: Anton Arnautov <arnautov.anton@gmail.com>
### 🎯 Goal Fix two UI regressions: - the attachment preview dismiss/remove icon was not respecting dark mode tokens - ChatView channel/thread toggling caused width jumps because the channel and thread sidebars used different width contracts ### 🛠 Implementation details - Remove the nested SVG color override from `src/components/MessageInput/styling/RemoveAttachmentPreviewButton.scss` so the icon inherits the existing close-control tokens. - Restore the failing `AttachmentPreviewList` test suite by fixing the missing React import in `PlayButton`, aligning the tests with the current media preview test IDs/override props, and seeding edited-message state through `messageComposer`. - Give `ThreadList` the same sidebar width/flex contract as `ChannelList`, and ensure the thread detail pane flexes correctly inside `ChatView.Threads`. - In the Vite example, define a shared `350px` sidebar baseline for both channel and thread views instead of separate ad hoc widths. ### 🎨 UI Changes - Attachment preview remove icons now inherit the correct dark mode colors. - Channel and thread sidebars now stay width-aligned when toggling in ChatView. - The sample app uses a shared `350px` sidebar baseline for both views.
### 🎯 Goal Align the media gallery viewer with the REACT-847 mock so the modal chrome, controls, and layout match the updated design. Linear ticket: https://linear.app/stream/issue/REACT-847/mediagallery-adjustments ### 🛠 Implementation details - Added a dedicated `GalleryHeader` component for the title, timestamp, download action, and close action. - Updated the gallery styling to use the full available modal area with an edge-to-edge blurred stage, overlay header controls, centered media index, and refined navigation buttons. - Added the new circular download icon and switched the gallery header to use it. - Removed the modal overlay close button so the gallery owns its close control. - Expanded gallery tests to cover the updated header actions and position indicator copy. ### 🎨 UI Changes Reference mock: https://linear.app/stream/issue/REACT-847/mediagallery-adjustments Local comparison screenshot was kept out of the commit and PR branch.
### 🎯 Goal - https://www.figma.com/design/Us73erK1xFNcB5EH3hyq6Y/Chat-SDK-Design-System?node-id=8262-118871&m=dev - https://www.figma.com/design/Us73erK1xFNcB5EH3hyq6Y/Chat-SDK-Design-System?node-id=8276-287624&m=dev - replaced LoadingIndicator with new icon, adjusted styling
### 🎯 Goal Implement the dynamic panel resizing ticket in the Vite example app as the first iteration before extracting the behavior into SDK components. Reference: the dynamic panel resizing ticket covering the channel list, message list, and thread panel behavior. Resolves: https://linear.app/stream/issue/REACT-828/layout-panel-resizing ### 🛠 Implementation details - persist left and thread panel layout in the existing example state store - split `examples/vite/src/App.tsx` layout concerns into focused `ChatLayout` modules for panel rendering, resize behavior, URL/state sync, and shared constants - add desktop drag resizing for the left sidebar and thread panel, including collapse and restore behavior, width constraints, and sidebar state synchronization with `navOpen` - keep the chat view selector stable on desktop and tune transition handling so drag remains responsive while automatic sidebar collapse still animates ### 🎨 UI Changes Example app layout behavior update: https://github.com/user-attachments/assets/e3e34721-d55a-44a7-a505-927f9df3ba2f
### 🎯 Goal Keep the React v14 migration audit in source control while the release is still moving, so follow-up breaking changes and doc rewrites can continue from a fixed baseline. ### 🛠 Implementation details - add `breaking-changes.md` as the confirmed v13 to v14 breaking-change tracker - add `docs-plan.md` as the page-by-page docs debt tracker and execution plan - record the currently audited SDK commit (`6ea7a78e4184fce6066f7318f9ebd57a5ff1474a`) in both files so future mining can start from `6ea7a78e..HEAD` - include the confirmed docs fallout and migration-guide inputs gathered during the audit - no tests run; tracker/docs files only ### 🎨 UI Changes N/A
### 🎯 Goal Notes for the updated v14 docs plan.
### 🎯 Goal Enable `exitSearchOnBlur` in the demo application, adjust the behavior accordingly (keep the search open if user clicks on filters or clear button as that could be still considered within the bounds of the search input behavior-wise).
BREAKING CHANGE: ChannelSearch component set has been dropped, use Search instead.
### 🎯 Goal Remove the deprecated `useAudioController` hook from the public API and eliminate the remaining source-level dependency on its file. BREAKING CHANGE: `useAudioController` has been removed from the public API. Migrate custom audio playback code to `useAudioPlayer`. ### 🛠 Implementation details - delete `src/components/Attachment/hooks/useAudioController.ts` - remove the public export from `src/components/Attachment/index.ts` - decouple `WaveProgressBar` from the deleted hook file by giving it its own UI-level seek callback type - keep the current audio playback components compatible with `audioPlayer.seek` - validated with `yarn types` ### 🎨 UI Changes None.
### 🎯 Goal https://linear.app/stream/issue/REACT-891/user-should-be-able-to-quote-a-message-with-a-voice-message Depends on: GetStream/stream-chat-js#1705 ### 🛠 Implementation details _Provide a description of the implementation_ ### 🎨 UI Changes _Add relevant screenshots_
### 🎯 Goal Fixes QA issues for edited indicator: https://linear.app/stream/issue/REACT-895/im-not-sure-i-understand-how-this-second-edited-label-works-or-should Docs: GetStream/docs-content#1085 ### 🛠 Implementation details - Removes the old `MessageEditedTimestamp` -> couldn't find a reference for it in docs - Adds a new component `MessageEditedIndicator` -> follows existing pattern to allow component overrides ### 🎨 UI Changes _Add relevant screenshots_
BREAKING CHANGE: replaced addNotification from ChannelStateContenxt with client.notifications API BREAKING CHANGE: NotificationList was moved with new parent now being MessageListMainPanel BREAKING CHANGE: rename MessageListNotificationsProps to NotificationListProps
BREAKING CHANGE: the BaseImage component uses ImageFallback component instead of CSS masl to display fallback on error
…#3207) PR #3128 renamed all SDK CSS custom properties with a --str-chat__ prefix immediately after PR #3133 synced the tutorial examples to the semantic token names. Update all five tutorial layout.css files accordingly. ### 🎯 Goal PR #3128 added the `--str-chat__` prefix to all SDK CSS custom properties, but the tutorial examples (steps 3–7) were not updated at the same time. As a result, the `stream-overrides` layer in each tutorial's `layout.css` sets properties that no longer exist, making the custom blue theme have no visible effect. ### 🛠 Implementation details Renamed all 13 CSS custom properties in the `.custom-theme` block across the five byte-identical `layout.css` files (tutorial steps 3–7) to match the current `--str-chat__` prefix convention (e.g. `--chat-bg-outgoing` → `--str-chat__chat-bg-outgoing`). ### 🎨 UI Changes CSS-only rename — no visual change intended. The theming was already broken before this fix; after the fix it renders as originally designed (navy outgoing bubbles, light-blue incoming bubbles, pale-blue panel backgrounds). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated tutorial example files across multiple sections to use standardized CSS variable naming conventions for theming and styling consistency. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Tai Nguyen <tainguyen@Tais-MacBook-Pro-M4.local>
### 🎯 Goal The vite example could only take the Stream API key from the `VITE_STREAM_API_KEY` env var, while the user `token` was already overridable via a query param. This makes it awkward to point the running example at a different Stream app without editing `.env` and restarting. This lets you pass `?api_key=…` (and combine it with `?token=…`) directly in the URL, matching how `token` already works. ### 🛠 Implementation details - `apiKey` now reads from the `api_key` query param first, falling back to `VITE_STREAM_API_KEY` — mirroring the existing `token` resolution. - `userId` now derives from the provided `token` (via `parseUserIdFromToken`) when present, slotted just below the explicit `user_id` query param and above the env/localStorage/random fallbacks. This makes `?api_key=…&token=…` self-sufficient: previously the pronto token generator was only skipped when the resolved `userId` matched the token's embedded `user_id`, so omitting `user_id` silently fell back to pronto with the wrong environment/app. - `parseUserIdFromToken` now returns `undefined` instead of throwing on a malformed/empty token, since it now runs during render; this also hardens the existing `tokenProvider` call site. User-id precedence: `?user_id=` → token-derived → `VITE_USER_ID` → `localStorage` → random. The explicit `user_id` param still wins, preserving the "connect as someone else, regenerate via pronto" path. ### 🎨 UI Changes No visual changes — this only affects how the example app sources its credentials. Verified by driving the running example with Playwright: - `?api_key=test_key_AAA&token=<jwt for "bob">` → Stream WS connects with `api_key=test_key_AAA` and `user_id=bob`; pronto skipped. - `?api_key=test_key_BBB` (no token) → pronto `create-token` called (fallback intact). - `?api_key=xzwhhgtazy6h` (public key) → real connection succeeds, chat UI renders, zero console errors. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * API key can now be optionally specified via query parameter, with fallback to environment configuration * Enhanced token parsing with improved error handling that gracefully manages malformed tokens <!-- end of auto-generated comment: release notes by coderabbit.ai -->
… rendering (#3209) ### 🎯 Goal Reactions sent via `channel.sendReaction` did not include `emoji_code`, so mobile push notification templates (e.g. for `reaction.new`) had no emoji to render. This wires the React SDK to include `emoji_code` in the reaction request payload so mobile SDKs can render the correct emoji in push notifications. Linear: [REACT-880](https://linear.app/stream/issue/REACT-880/add-emoji-code-to-reaction-request-payload) ### 🛠 Implementation details - New exported helper `getEmojiCodeByReactionType(reactionOptions, type)` in `reactionOptions.tsx` resolves the native emoji character (e.g. `👍`) for a reaction type from the option's existing `unicode` field via `unicodeToEmoji()`. Returns `undefined` for legacy array options or options that omit `unicode`, so we never send a garbage code. - `useReactionHandler` now reads `reactionOptions` from `ComponentContext` (falling back to `defaultReactionOptions`, like other consumers) and includes `emoji_code` in the `sendReaction` payload when one resolves: ```ts channel.sendReaction(id, { type, ...(emojiCode && { emoji_code: emojiCode }) }); ``` The optimistic reaction preview is stamped with the same value for consistency. The public `handleReaction(type, event)` signature is **unchanged**. - `mapEmojiMartData` now stores `unicode` on each mapped entry, so emoji picked from the **extended** (emoji-mart) list also resolve an `emoji_code`. - The `stream-chat` `Reaction` type already supports the optional `emoji_code` field — only stream-chat-react needed to populate it. **Behavior:** default reactions (`like`, `love`, `haha`, `sad`, `wow`, `fire`) and any custom reactions defining `unicode` now ship `emoji_code` automatically. Legacy array-form reaction options carry no unicode data and continue to send no `emoji_code` (graceful, unchanged otherwise). **Tests:** new `reactionOptions.test.ts` covering the helper (quick/extended/legacy-array/unknown/missing-unicode) and `mapEmojiMartData`; `useReactionHandler` tests for default/custom/no-unicode payloads and the optimistic preview; updated the existing `Message.test.tsx` send assertion to the new contract. ### 🎨 UI Changes None — this only affects the network payload sent with reactions (and the local optimistic reaction object). No visual changes. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Reactions now support and transmit emoji code data for improved emoji handling. * **Tests** * Expanded test coverage for reaction emoji code derivation with custom reaction options. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## [14.4.0](v14.3.0...v14.4.0) (2026-06-05) ### Bug Fixes * **compat:** restore React 17/18 compatibility in certain components ([#3197](#3197)) ([b513b69](b513b69)) * general performance and bug fixes ([#3201](#3201)) ([57c5795](57c5795)) * **interop:** unwrap CJS default exports (react-player, @emoji-mart/react) ([#3199](#3199)) ([4cddb02](4cddb02)) * maintain topmost modal non-inert ([#3206](#3206)) ([7ad98fa](7ad98fa)) ### Features * allow to stack modals on top of each other ([#3203](#3203)) ([4c934ae](4c934ae)) * display notifications above modals ([#3200](#3200)) ([0433090](0433090)) * **Reactions:** send emoji_code with reactions for push notification rendering ([#3209](#3209)) ([2faa620](2faa620)) ### Performance Improvements * **Message:** hoist regex compilation in message text rendering ([#3202](#3202)) ([8c018a4](8c018a4)) * **VideoPlayer:** lazy-load react-player to keep it out of the main bundle ([#3204](#3204)) ([18dc966](18dc966))
…3211) ### 🎯 Goal `postinstall` ran `husky` unconditionally. npm executes a dependency's `postinstall` for every consumer, but `husky` is a `devDependency` and isn't installed in a consumer's tree, so `npm install stream-chat-react` failed: ``` npm error code 127 npm error sh: husky: command not found ``` This ports the same fix already merged for the JS SDK in [GetStream/stream-chat-js#1764](GetStream/stream-chat-js#1764) — `stream-chat-react` had the identical `"postinstall": "husky"`. ### 🛠 Implementation details **Why husky has to stay in `postinstall`:** Yarn Berry (this repo's package manager) runs the **root workspace's `postinstall`** on `yarn install` but **not** its `prepare`. Moving husky to `prepare` would silently stop installing contributors' git hooks. husky must stay in `postinstall`; the fix is to stop *running* it for consumers. - `postinstall` now invokes a dev-only `scripts/install-husky.mjs` **only when it is present** — and that file is intentionally **not** in `package.json#files` (which publishes only `dist`, `package.json`, `README.md`, `AI.md`), so consumers never receive it and the guard short-circuits to a no-op. - The invocation runs via `node` (not a shell `|| true`) so it's cross-platform — npm runs consumer postinstalls through `cmd.exe` on Windows, where `||`/`true` aren't valid. - `scripts/install-husky.mjs` runs husky only inside a git checkout, and lets genuine husky setup errors **surface** (not swallowed) so contributors learn if hook installation actually fails. > Note: unlike the JS SDK PR (which left `prepare: yarn run build` untouched), this repo uses `prepack: yarn build` and has no `prepare` script, so nothing there needed changing — the husky fix is independent of it. **Verified locally:** | Scenario | Result | | --- | --- | | Consumer (no `scripts/install-husky.mjs`) | exit 0 · husky never executed | | Contributor (`.git` + script present) | `core.hooksPath=.husky/_` · hooks installed · exit 0 | `yarn prettier` and `yarn eslint` clean on both changed files. ### 🎨 UI Changes None — build/install tooling only. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Improved post-install script configuration to make Git hook initialization more reliable and conditional based on repository presence. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## [14.4.1](v14.4.0...v14.4.1) (2026-06-05) ### Bug Fixes * only run husky in a git checkout so npm consumers don't break ([#3211](#3211)) ([640bf4b](640bf4b))
…ine predicate (#3213) ## Summary - In `5-custom-attachment-type/App.tsx`, `hasProductMessage` used an inline predicate `(attachment) => 'type' in attachment && attachment.type === 'product'` that duplicated the existing `isProductAttachment` type guard defined earlier in the same file. - Replaced the inline predicate with a direct reference to `isProductAttachment`. ## Test plan - [ ] No behaviour change — purely a refactor to reuse the existing type guard. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Documentation * **Examples**: Updated tutorial to demonstrate improved patterns for detecting and handling custom attachment types in message processing workflows. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Tai Nguyen <tainguyen@Tais-MacBook-Pro-M4.local>
### 🎯 Goal
Message timestamps were not fully localized. Most visibly, the
message-reminder due-date label rendered in **English for every
non-English locale** — e.g. Spanish showed `Today at 15:00` / `Yesterday
at 09:30` instead of `Hoy a las 15:00` / `Ayer a las 09:30`.
Root cause: `timestamp/ReminderNotification` was a verbatim English copy
of the `en` value in all 11 non-English locale files. Every *other*
calendar key (`DateSeparator`, `ChannelPreviewTimestamp`) was properly
translated; this one was missed when the reminder feature landed.
`scripts/validate-translations.js` only fails on **empty** values, so an
English-leak passes CI — which is why it went unnoticed.
A broader pass also surfaced two more English-leaking timestamp keys
(`relativeDaysAgo` / `relativeWeeksAgo`, the compact "ago" form used
e.g. for poll-vote ages) and a few stray untranslated strings.
### 🛠 Implementation details
- **`timestamp/ReminderNotification`** (all 11 non-English locales):
localized the `calendarFormats` literals.
- Day-words taken from each locale's already-shipped
`timestamp/DateSeparator` translation.
- "at"-connectors taken from the dayjs `updateLocale` calendar configs
in `Streami18n.ts` (es `a las`, de `um`, fr `à`, it `alle`, pt `às`, nl
`om`, ru `в`, tr `saat`; ja/ko/hi use none).
- `lastWeek` patterns mirror the grammatically-correct forms already
defined in `Streami18n.ts` (e.g. de `letzten dddd`, fr `dddd dernier`,
pt `dddd passada`, it `lo scorso dddd`; ru drops the un-agreeable
prefix, matching the source which leaves ru `lastWeek` undefined).
- **`timestamp/relativeDaysAgo` / `timestamp/relativeWeeksAgo`** (all
11): localized the compact "N days/weeks ago" forms per language — e.g.
es `hace {{ count }} d` / `sem`, de `vor {{ count }} Tg.` / `Wo.`, fr
`il y a {{ count }} j` / `sem`, ja `{{ count }}日前` / `週間前`, ru `{{ count
}} дн.` / `нед. назад`.
- **Remaining gaps:** de `Back`→`Zurück`, `Instant
commands`→`Sofortbefehle`, `Search GIFs`→`GIFs suchen`, capitalized
German plural nouns (`{{ count }} Links`, `{{ count }} Videos`); tr
`language/ha`→`Hausaca` (the only break in Turkish's systematic
`-ca/-ce` language-name pattern).
- **Left unchanged:** correct cognates/loanwords that merely coincide
with English (fr `Menu`/`Image`/`Photo`/`Question`/`Votes`, de/nl
`Online`/`Offline`/`Link`/`Video`/`Original`, and the `language/*` names
that are genuinely identical in those languages).
**Verification:** all 12 locale JSONs valid · `validate-translations`
passes · prettier clean · `i18n` + `ReminderNotification` +
`MessageTimestamp` tests pass (130) · rendered output spot-checked
through dayjs `.calendar()` per locale. Reviewed via Codex; both
findings (lastWeek grammar, audit-helper) addressed.
### 🎨 UI Changes
Text-only — no structural or visual changes, so no screenshots. Example
before → after:
| Surface | Locale | Before | After |
| --- | --- | --- | --- |
| Reminder due date | es | `Today at 15:30` | `Hoy a las 15:30` |
| Reminder due date | de | `Today at 15:30` | `Heute um 15:30` |
| Reminder (last week) | fr | `Last Sunday at 15:30` | `dimanche dernier
à 15:30` |
| Poll-vote age | es | `2d ago` | `hace 2 d` |
| Poll-vote age | ja | `2d ago` | `2日前` |
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Documentation**
* Enhanced internationalization with corrected and improved translations
across 11 languages (German, Spanish, French, Hindi, Italian, Japanese,
Korean, Dutch, Portuguese, Russian, and Turkish).
* Updated date and time formatting to display more naturally and
consistently in each language's locale.
* Fixed translation inconsistencies for UI labels and relative time
expressions throughout the interface.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
…revent InvalidStateError (#3218)
### 🎯 Goal A mirror of [this PR](GetStream/stream-chat-react-native#3648) from the React Native SDK. ### 🛠 Implementation details _Provide a description of the implementation_ ### 🎨 UI Changes _Add relevant screenshots_ <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * The message composer now uses the shared client-side cache provided by the chat client, improving consistency across views and avoiding duplicate cache setup. * **Chores** * Bumped `stream-chat` to a newer patch version in the main project and example packages. * Updated package manager configuration to explicitly allow `stream-chat`. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Anton Arnautov <arnautov.anton@gmail.com> Co-authored-by: Oliver Lazoroski <oliver.lazoroski@gmail.com> Co-authored-by: Anton Arnautov <43254280+arnautov-anton@users.noreply.github.com>
## [14.5.0](v14.4.1...v14.5.0) (2026-06-19) ### Bug Fixes * **i18n:** localize message timestamps across all locales ([#3220](#3220)) ([c1b65b6](c1b65b6)) * **MediaRecorder:** guard pause/resume against inactive recorder to prevent InvalidStateError ([#3218](#3218)) ([a7e9006](a7e9006)) * use client mandated composer queue ([#3222](#3222)) ([57906bb](57906bb)) ### Features * add enhanced mentions ([#3189](#3189)) ([7172ebc](7172ebc)) ### Refactors * **tutorial:** use isProductAttachment type guard instead of inline predicate ([#3213](#3213)) ([0b95202](0b95202))
## [14.6.0](v14.5.0...v14.6.0) (2026-06-25) ### Features * add ChannelDetail component ([#3221](#3221)) ([2312d89](2312d89))
## Summary URLs written with an uppercase scheme (e.g. `HTTPS://example.com`) were rendered incorrectly in message text: - The anchor was emitted **without** the `str-chat__message-url-link` class, so the link looked like plain, unstyled text (effectively "not recognized as a link"). - The scheme was **not stripped** from the displayed text, so users saw `HTTPS://example.com` instead of `example.com`. The lowercase equivalent (`https://example.com`) renders correctly, so the bug is purely about case sensitivity in two helpers. ## Root cause Two case-sensitive helpers in the `renderText` pipeline: 1. **`componentRenderers/Anchor.tsx`** decided whether to apply the link class with `href?.startsWith('http')`. An `HTTPS://` href fails this check, so `isUrl` was `false` and the `str-chat__message-url-link` class was dropped. 2. **`regex.ts`** strips the scheme for display text via `detectHttp = /(http(s?):\/\/)?(www\.)?/`. Without the `i` flag it doesn't match `HTTPS://`, so the scheme stayed in the display text. `linkifyjs` (which detects the URL) and `react-markdown` (which renders it) are both already case-insensitive, so they are not at fault — they correctly identify and preserve the uppercase-scheme URL. The display/styling layer was the only case-sensitive part. ## The fix ```diff - const isUrl = href?.startsWith('http'); + const isUrl = href?.toLowerCase().startsWith('http'); ``` ```diff - export const detectHttp = /(http(s?):\/\/)?(www\.)?/; + export const detectHttp = /(http(s?):\/\/)?(www\.)?/i; ``` Both changes are minimal and preserve the existing behavior for lowercase schemes. ## Reproduction Before the fix: - `https://en.wikipedia.org/wiki/Apple` -> styled link, display text `en.wikipedia.org/wiki/Apple` - `HTTPS://en.wikipedia.org/wiki/Apple` -> unstyled `<a>` (no `str-chat__message-url-link`), display text `HTTPS://en.wikipedia.org/wiki/Apple` After the fix, the uppercase variant renders identically to the lowercase one: a styled link with the scheme stripped from the display text, while the `href` keeps its original casing. ## Tests Added a regression test in `renderText.test.tsx` asserting that `"see HTTPS://en.wikipedia.org/wiki/Apple"` produces an anchor with: - class `str-chat__message-url-link` - `href="HTTPS://en.wikipedia.org/wiki/Apple"` (original casing preserved) - link text `en.wikipedia.org/wiki/Apple` (scheme stripped) The test fails on `master` (the input renders as plain text with no anchor) and passes with this change. The full `renderText` suite (61 tests) passes; no existing snapshots changed. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * URLs with uppercase or mixed-case schemes are now detected and linked correctly. * Link rendering now treats URL schemes case-insensitively, improving consistency for pasted links. * Added test coverage for uppercase URL matching and link behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
### 🎯 Goal Fixes three Tier‑1 issues from the bug‑bash triage. Each is an independent fix kept as its own self‑contained commit (with tests): - **Closes #2474** — a `message.new` for a not‑yet‑watched channel inserts an uninitialized channel into the list; rendering it called `channel.muteStatus()`, which throws `"hasn't been initialized yet"` and **crashed the whole app**. - **Closes #2441** — `ChannelList` issued a `queryChannel` (`channel.watch()`) for **every** `notification.message_new`, even when the result was discarded, exhausting the rate limit during bulk channel activity. - **Closes #2393** — once the shared client is disconnected, `channel.lastRead()` (called during render in `useCreateChannelStateContext`) throws `"You can't use a channel after client.disconnect() was called"`, **crashing the render** with no error boundary to catch it. > **#2599 — fixed via the `stream-chat` upstream fix + this PR's dependency bump.** An earlier `stopWatching`-on-removal attempt here was implemented and then reverted: it did **not** make removal durable. `channel.stopWatching()` does not evict the channel from `client.activeChannels`, so a reconnect (`recoverState` re-watches every active channel) or a queued/in-flight `message.new` re-added the removed channel — the exact reported symptom. The real fix evicts `activeChannels[cid]` on `notification.removed_from_channel` (mirroring the existing `channel.deleted` handling); it shipped in **`stream-chat@9.49.0`** (GetStream/stream-chat-js#1788). This PR bumps the `stream-chat` requirement to `^9.49.0` and adds a regression test — with the bump in place, `ChannelList` removal is durable and the test passes. ### 🛠 Implementation details **#2474 — `fix(ChannelListItem): guard muteStatus against an uninitialized/disconnected channel`** - `useIsChannelMuted` reads the mute status through a helper that calls `channel.muteStatus()` only when the channel is initialized **and not disconnected**, falling back to a not‑muted status otherwise. `muteStatus()` throws on an uninitialized channel (`#2474`) and also after the client disconnects — via `channel.getClient()` — even though the channel stays initialized (the #2393 failure class on the channel‑list path). - The mute‑update subscription now depends on `[channel, client]` and cleans up via the `unsubscribe` handle returned by `client.on()`. It previously depended on `[muted]`, so it re‑subscribed on its own output and could capture a stale channel. **#2441 — `fix(ChannelList): do not query unfiltered channels on notification.message_new`** - Moved the `allowNewMessagesFromUnfilteredChannels` guard **before** the `getChannel()` call in `handleNotificationMessageNew`, so the flag actually suppresses the per‑event `watch()` instead of querying first and throwing the result away. Behaviour with the flag enabled is unchanged. **#2393 — `fix(Channel): guard channel methods against a disconnected client`** - Guarded `channel.lastRead()` in `useCreateChannelStateContext` with `!channel.disconnected` so a disconnect while the channel is mounted does not crash the render. - `loadMore` now early‑returns when `channel.disconnected`, mirroring the existing `markRead` guard, to avoid a doomed pagination query. - Note: the shared‑client race cannot be fully eliminated (the existing `try/catch` backstops on the async paths remain); this removes the uncaught **render‑time** crash, which is the part with no safety net. **#2599 — `fix(deps): require stream-chat ^9.49.0 to evict removed channels`** - Bumped `stream-chat` to `^9.49.0` in `peerDependencies` and `devDependencies`. No `ChannelList` runtime change is required: the existing `handleNotificationRemovedFromChannel` removes the channel from the list, and `stream-chat@9.49.0` now evicts it from `client.activeChannels`, so `recoverState()` won't re‑watch it on reconnect. The two compose into durable removal. **Tests** — added test‑first (each test was confirmed to fail before the fix): - `useIsChannelMuted` does not throw on an uninitialized channel, nor on an initialized‑but‑disconnected channel. - `notification.message_new` does not call `watch()` when `allowNewMessagesFromUnfilteredChannels={false}`. - rendering does not crash when the client disconnects while the channel is mounted. - `loadMore` does not query when the client is disconnected. - **#2599:** `notification.removed_from_channel` evicts the channel from `client.activeChannels` (so reconnect cannot re‑watch it). Verified red→green — red on `stream-chat@9.47.0`, green after the `^9.49.0` bump. Verification: `yarn test` (full suite — 2589 passing, incl. ChannelList, ChannelListItem, Channel, MessageList, Thread), `yarn types`, and ESLint (`--max-warnings 0`) all clean. #2474, #2441 and #2393 were additionally confirmed in a real browser against the example app; the #2599 regression test passes against the bumped `stream-chat@9.49.0`. ### 🎨 UI Changes None — these are crash / correctness / rate‑limit fixes in `ChannelList` and `Channel` event handling, with no visual changes. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Fixed message-new handling when unfiltered channels aren’t allowed so unlisted/unqueried channels aren’t watched or added to the list. * Made mute status lookups safe when a channel isn’t initialized or is disconnected. * Prevented pagination and last-read handling from running while the client/channel is disconnected to avoid crashes. * **Tests** * Added coverage for message-new filtering with unfiltered channels disabled. * Expanded mute hook tests for uninitialized and disconnected cases. * Added disconnected-client scenarios for rendering and pagination. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
… ESM (#3231) ### 🎯 Goal The published ESM bundle imported dayjs plugins and locales via **extensionless subpaths** (e.g. `import 'dayjs/plugin/calendar'`, `import 'dayjs/locale/de'`). dayjs ships no `exports` map, so Node's **native ESM resolver requires the file extension** and rejects these specifiers with `ERR_MODULE_NOT_FOUND`. Bundlers (Turbopack, Webpack, Vite) resolve extensionless subpaths fine, so the bug is invisible in most setups. But Node's native loader does not — and Next.js 16 loads `stream-chat-react` as a **server external** during "Collecting page data", so the consumer build fails: ``` Failed to load external module stream-chat-react...: ERR_MODULE_NOT_FOUND Cannot find module '.../dayjs/plugin/calendar' imported from .../useNotificationApi.<hash>.mjs Did you mean to import "dayjs/plugin/calendar.js"? ``` **Regressed in #3188 — first shipped in `14.4.0`.** Verified by loading each published `dist/es/index.mjs` under Node's native ESM loader: `14.3.0` loads cleanly, `14.4.0` (and every release since) throws `ERR_MODULE_NOT_FOUND → dayjs/plugin/calendar`. #3188 broadened the Vite `external` subpath regex from `(\/[\w-]+)?` (one segment) to `(\/.+)?` (any depth). `dayjs/plugin/calendar` is a two-segment subpath, so the old regex didn't match it and Vite **bundled** the plugin inline (no bare import in the output); the new regex **externalizes** it, emitting the extensionless specifier verbatim. The extensionless imports had always been in source — the build used to bundle them away. Note that broadening the regex was itself a deliberate ESM-correctness fix (keeping CJS `require()` glue out of the ESM output), so externalizing subpaths is intended. This PR keeps the externalization and makes the specifiers valid, rather than reverting it and reintroducing the CJS-glue problem. ### 🛠 Implementation details **The fix** — add `.js` to every dayjs subpath import in shipped source, so the specifiers Vite externalizes (emits verbatim) are valid ESM: - `src/i18n/Streami18n.ts` — 8 plugin imports + 12 locale side-effect imports - `src/context/TranslationContext.tsx` — 2 plugin imports - `src/i18n/utils.ts` — 1 type-only import (keeps the emitted `.d.ts` consistent) - also corrected a stale JSDoc example and the `registerTranslation` log message `.js` is safe across every consumer: Node ESM (the fix), bundlers, and TS types (`moduleResolution: "bundler"` maps `.js` → `.d.ts`). The CJS output is unaffected. **Regression guard** — the existing `validate-cjs` smoke test loads the bundle with `require()`, whose CJS resolver tolerates extensionless imports, so it never caught this. Added the missing ESM counterpart: - `scripts/validate-esm-node-bundle.mjs` — imports `dist/es/index.mjs` under Node's native loader - `yarn validate-esm` script + a CI step in `ci.yml` right after `validate-cjs` Verified: `validate-esm` **passes** on a healthy build and **fails with `ERR_MODULE_NOT_FOUND`** when a `.js` is stripped from a dayjs import in the built output. `yarn build`, `yarn types`, ESLint, and the i18n tests all pass. **Also included — toolchain bump** (independent of the fix, bundled here): `vite` 8.0.14 → 8.1.3, `vitest` + `@vitest/coverage-v8` 4.1.7 → 4.1.9, `@vitest/eslint-plugin` 1.6.18 → 1.6.20 (pulls rolldown 1.0.2 → 1.1.3); the `examples/*` workspaces bumped to match; `.yarnrc.yml` lowers `npmMinimalAgeGate` to `1d` and preapproves `vite`, and drops `enableHardenedMode`. Diffing `dist/es` + `dist/cjs` before/after the bump shows **no behavioral or API-surface change** — only additional `/* @__PURE__ */` annotations and one internal import alias, both from the rolldown bump. ### 🎨 UI Changes None — packaging/build fix, no runtime or visual behavior change. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved ESM bundle compatibility by updating import paths to use explicit `.js` extensions. * Added validation to catch Node ESM import issues earlier during CI. * Updated user-facing guidance for locale imports to match the new ESM format. * **Chores** * Updated several build and test tool versions. * Adjusted package manager settings and example project dependencies for the latest releases. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## [14.6.1](v14.6.0...v14.6.1) (2026-07-03) ### Bug Fixes * bug bashing ChannelList + Channel ([#2474](#2474), [#2441](#2441), [#2393](#2393)) ([#3227](#3227)) ([f790520](f790520)), closes [GetStream/stream-chat-js#1788](GetStream/stream-chat-js#1788) [#2599](#2599) [#2599](#2599) * **i18n:** use .js extensions on dayjs subpath imports for valid Node ESM ([#3231](#3231)) ([7663b18](7663b18)), closes [#3188](#3188) [#3188](#3188) * **MessageList:** don't count thread replies toward channel unread UI state ([#3229](#3229)) ([ca5ed16](ca5ed16)) * **renderText:** recognize uppercase URL schemes ([#3226](#3226)) ([21d57e3](21d57e3)) * **renderText:** recognize uppercase URL schemes in message links ([6178513](6178513)) * return background colors to LinkPreviewCard and TypingIndicator ([a768e30](a768e30))
# Conflicts: # .cursor/skills/ralph-protocol/SKILL.md # examples/vite/src/App.tsx # examples/vite/src/AppSettings/tabs/Reactions/ReactionsTab.tsx # examples/vite/src/index.scss # examples/vite/src/stream-imports-layout.scss # src/components/Attachment/Audio.tsx # src/components/Attachment/Geolocation.tsx # src/components/Attachment/Giphy.tsx # src/components/Attachment/__tests__/Attachment.test.tsx # src/components/Attachment/__tests__/Audio.test.tsx # src/components/Attachment/__tests__/Card.test.tsx # src/components/Attachment/__tests__/VoiceRecording.test.tsx # src/components/Attachment/hooks/useAudioController.ts # src/components/Channel/Channel.tsx # src/components/Channel/__tests__/Channel.test.tsx # src/components/Channel/channelState.ts # src/components/Channel/hooks/useCreateChannelStateContext.ts # src/components/Channel/hooks/useMentionsHandlers.ts # src/components/Channel/utils.ts # src/components/ChannelHeader/ChannelHeader.tsx # src/components/ChannelHeader/__tests__/ChannelHeader.test.tsx # src/components/ChannelHeader/hooks/useChannelHeaderOnlineStatus.ts # src/components/ChannelList/ChannelList.tsx # src/components/ChannelList/__tests__/ChannelList.test.tsx # src/components/ChannelList/index.ts # src/components/ChannelListItem/ChannelListItem.tsx # src/components/ChannelListItem/__tests__/ChannelListItem.test.tsx # src/components/ChannelPreview/ChannelPreviewMessenger.tsx # src/components/ChannelPreview/__tests__/ChannelPreviewMessenger.test.js # src/components/ChannelSearch/hooks/useChannelSearch.ts # src/components/Chat/Chat.tsx # src/components/Chat/__tests__/Chat.test.tsx # src/components/Chat/hooks/useChat.ts # src/components/Chat/hooks/useCreateChatContext.ts # src/components/ChatView/ChatView.tsx # src/components/ChatView/__tests__/ChatView.test.tsx # src/components/ChatView/styling/ChatView.scss # src/components/Gallery/__tests__/Image.test.js # src/components/Icons/createIcon.tsx # src/components/MediaRecorder/AudioRecorder/__tests__/AudioRecorder.test.js # src/components/MediaRecorder/hooks/useMediaRecorder.ts # src/components/Message/Message.tsx # src/components/Message/MessageAlsoSentInChannelIndicator.tsx # src/components/Message/MessageStatus.tsx # src/components/Message/MessageUI.tsx # src/components/Message/QuotedMessage.tsx # src/components/Message/__tests__/Message.test.tsx # src/components/Message/__tests__/MessageDeleted.test.js # src/components/Message/__tests__/MessageRepliesCountButton.test.tsx # src/components/Message/__tests__/MessageText.test.js # src/components/Message/__tests__/MessageUI.test.tsx # src/components/Message/__tests__/QuotedMessage.test.js # src/components/Message/hooks/__tests__/useActionHandler.test.tsx # src/components/Message/hooks/__tests__/useDeleteHandler.test.tsx # src/components/Message/hooks/__tests__/useFlagHandler.test.js # src/components/Message/hooks/__tests__/useMentionsHandler.test.tsx # src/components/Message/hooks/__tests__/useMuteHandler.test.js # src/components/Message/hooks/__tests__/useOpenThreadHandler.test.tsx # src/components/Message/hooks/__tests__/useReactionHandler.test.tsx # src/components/Message/hooks/__tests__/useRetryHandler.test.tsx # src/components/Message/hooks/__tests__/useUserRole.test.tsx # src/components/Message/hooks/__tests__/userMarkUnreadHandler.test.js # src/components/Message/hooks/useDeleteHandler.ts # src/components/Message/hooks/useMarkUnreadHandler.ts # src/components/Message/hooks/useMuteHandler.ts # src/components/Message/hooks/usePinHandler.ts # src/components/Message/hooks/useReactionHandler.ts # src/components/Message/hooks/useUserRole.ts # src/components/Message/styling/QuotedMessage.scss # src/components/Message/types.ts # src/components/MessageActions/__tests__/MessageActions.test.js # src/components/MessageActions/defaults.tsx # src/components/MessageActions/hooks/useBaseMessageActionSetFilter.ts # src/components/MessageComposer/AttachmentSelector/AttachmentSelector.tsx # src/components/MessageComposer/EditMessageForm.tsx # src/components/MessageComposer/MessageComposerActions.tsx # src/components/MessageComposer/QuotedMessagePreview.tsx # src/components/MessageComposer/__tests__/AttachmentSelector.test.tsx # src/components/MessageComposer/__tests__/ThreadMessageInput.test.tsx # src/components/MessageComposer/hooks/index.ts # src/components/MessageComposer/hooks/useCooldownTimer.tsx # src/components/MessageComposer/hooks/useMessageComposerBindings.ts # src/components/MessageComposer/hooks/useMessageComposerController.ts # src/components/MessageComposer/hooks/useSendMessageFn.ts # src/components/MessageComposer/hooks/useSubmitHandler.ts # src/components/MessageComposer/hooks/useUpdateMessageFn.ts # src/components/MessageComposer/styling/SendToChannelCheckbox.scss # src/components/MessageInput/hooks/__tests__/useCooldownTimer.test.js # src/components/MessageList/MessageList.tsx # src/components/MessageList/MessageListNotifications.tsx # src/components/MessageList/ScrollToLatestMessageButton.tsx # src/components/MessageList/UnreadMessagesNotification.tsx # src/components/MessageList/UnreadMessagesSeparator.tsx # src/components/MessageList/VirtualizedMessageList.tsx # src/components/MessageList/VirtualizedMessageListComponents.tsx # src/components/MessageList/__tests__/MessageList.test.js # src/components/MessageList/__tests__/ScrollToLatestMessageButton.test.tsx # src/components/MessageList/__tests__/VirtualizedMessageListComponents.test.tsx # src/components/MessageList/__tests__/utils.test.ts # src/components/MessageList/hooks/MessageList/useFloatingDateSeparatorMessageList.ts # src/components/MessageList/hooks/MessageList/useMessageListScrollManager.ts # src/components/MessageList/hooks/MessageList/useScrollLocationLogic.tsx # src/components/MessageList/hooks/__tests__/useMarkRead.test.tsx # src/components/MessageList/hooks/useMarkRead.ts # src/components/Modal/CloseButtonOnModalOverlay.tsx # src/components/Poll/PollActions/PollResults/PollOptionWithVotes.tsx # src/components/Poll/PollCreationDialog/PollCreationDialogControls.tsx # src/components/Poll/PollOptionSelector.tsx # src/components/Poll/__tests__/Poll.test.tsx # src/components/Poll/__tests__/PollActions.test.tsx # src/components/Poll/__tests__/PollOptionList.test.tsx # src/components/Reactions/ReactionSelectorWithButton.tsx # src/components/Search/SearchResults/SearchResultItem.tsx # src/components/Search/__tests__/SearchResultItem.test.tsx # src/components/TextareaComposer/TextareaComposer.tsx # src/components/Thread/Thread.tsx # src/components/Thread/ThreadHead.tsx # src/components/Thread/ThreadHeader.tsx # src/components/Thread/ThreadStart.tsx # src/components/Thread/__tests__/Thread.test.tsx # src/components/Thread/__tests__/ThreadStart.test.tsx # src/components/Threads/ThreadContext.tsx # src/components/Threads/ThreadList/ThreadList.tsx # src/components/Threads/ThreadList/ThreadListItemUI.tsx # src/components/Threads/ThreadList/__tests__/ThreadList.test.tsx # src/components/TypingIndicator/TypingIndicator.tsx # src/components/TypingIndicator/__tests__/TypingIndicator.test.js # src/components/Window/__tests__/Window.test.tsx # src/context/ChannelActionContext.tsx # src/context/ChannelStateContext.tsx # src/context/ChatContext.tsx # src/context/MessageBounceContext.tsx # src/context/MessageContext.tsx # src/context/index.ts # src/i18n/de.json # src/i18n/en.json # src/i18n/es.json # src/i18n/fr.json # src/i18n/hi.json # src/i18n/it.json # src/i18n/ja.json # src/i18n/ko.json # src/i18n/nl.json # src/i18n/pt.json # src/i18n/ru.json # src/i18n/tr.json # src/stories/add-message.stories.tsx # src/stories/attachment-sizing.stories.tsx # src/stories/edit-message.stories.tsx # src/stories/jump-to-message.stories.tsx # src/stories/mark-read.stories.tsx # src/stories/navigate-long-message-lists.stories.tsx # src/stories/pin-message.stories.tsx
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Depends on
GetStream/stream-chat-js#1795
Summary
This PR is a major architecture migration, not just message pagination.
It moves chat/thread runtime behavior from React-owned channel contexts to instance APIs in
stream-chat-js, introduces a slot-based ChatView layout controller, and rewires navigation/state to reactiveStateStore-driven sources (messagePaginator,configState,threads.state, etc.).Scope
ChannelActionContextandChannelStateContextruntime usage with instance APIs fromstream-chat-js.messagePaginatorand client instance APIs.configState.requestHandlers.What Changed
1) New ChatView Layout Control API
LayoutControllerstate model for slot topology, bindings, visibility, and per-slot history.ChatViewNavigationAPI:openView(view, { slot? })openChannel(channel, { slot? })closeChannel({ slot? })openThread(threadOrTarget, { slot? })closeThread({ slot? })hideChannelList({ slot? })unhideChannelList({ slot? })ChannelSlotThreadSlotChannelListSlotThreadListSlotuseSlotEntity,useSlotChannel,useSlotThreadserializeLayoutStaterestoreLayoutStatelayoutSlotResolversfor deterministic slot targeting.2) Channel/Thread Ownership Moved to stream-chat-js Instances
messagePaginatorand thread manager state.StateStore.useChatViewNavigation()(with legacy fallback deactivation inThread).3) Context Removal and Replacement
ChannelActionContextChannelStateContextTypingContextprovider path from Channel runtimeChannelInstanceContext+useChannel()as the channel resolution contract.useChannel()resolves from:ChannelInstanceContext4) Message Pagination + Unread/Focus Migration
useMessagePaginator()messagePaginator.jumpToMessage(...)messagePaginator.jumpToTheFirstUnreadMessage(...)messagePaginator.jumpToTheLatestMessage()messagePaginator.toHead()/toTail()messagePaginator.ingestItem(...)messagePaginator.removeItem(...)messagePaginator.unreadStateSnapshotclient.messageDeliveryReporterinstead of context actions.5) Request Handler Customization Moved to Instance Config
channel.configState.requestHandlersthread.configState.requestHandlersAPI Changes and Migration Guide
Navigation:
setActiveChannel/context actions -> ChatView navigationMessage jump/pagination: context methods -> paginator methods
Channel access: ChannelState/Action context -> useChannel()
Slot-based rendering (new recommended pattern)
Behavioral Notes
Testing
Breaking/Important for Integrators
🎨 UI Changes
No planned UI changes