chore: merge master (14.11.0) into release-v15 - #3260
Conversation
β¦tutorial (#3251) ### π― Goal `examples/tutorial` had drifted out of sync with the published [React chat tutorial](https://getstream.io/chat/sdk/react/tutorial/), which was restructured in GetStream/getstream.io#345 into numbered steps 0-7 plus two optional recipes. Folder numbering no longer lined up with the tutorial's step numbers, and there was no runnable counterpart for the theming step at all. Two things also made the example unusable as it stood: - It did not boot on a clean install. Two React instances, `Invalid hook call` on first render. Reproduces on `master`. - The preview panel clipped the chat UI at the edges and pushed the composer below the fold. The point of this example is that a reader can run the exact code the tutorial gave them, so a folder here needs to map 1:1 to a heading there. ### π Implementation details Three commits, each independently revertable. **1. Step alignment** (`244f77697`) Folders renumbered to match the tutorial, and the two non-linear steps renamed to `optional-*`: | Folder | Tutorial section | | --- | --- | | `2-client-setup` | Step 2 - Connect the client | | `3-core-component-setup` | Step 3 - Get a working chat UI | | `4-channel-list` | Step 4 - Add a channel list | | `5-theming` | Step 5 - Theme it (**new**) | | `6-custom-ui-components` | Step 6 - Replace an SDK component | | `7-emoji-picker` | Step 7 - Enable the emoji picker and autocomplete | | `optional-custom-attachment-type` | Optional - add a custom attachment type | | `optional-livestream` | Optional - a livestream-style chat app | The tutorial's Step 0 (environment) and Step 1 (project + credentials) have no runnable counterpart, so numbering starts at 2. Also in this commit: - **Preview panel layout fix.** The chrome panes are sized in viewport units *and* padded, so under the default `content-box` the padding was added on top of `100vh` β 1312px of content in a 1272px viewport. Fixed with `border-box` on the six named chrome classes only, deliberately not `.tutorial-browser *`, so the SDK's own box-sizing is untouched. - **Entry cleanup.** Removed the 16 per-step `main.tsx` / `index.html` files. They date to #2697, when the step browser did not exist and booting a step's own HTML was the only way to run it. Since the browser landed they have been dead weight: `vite build` only ever emitted `dist/index.html`, nothing referenced them, and all eight `main.tsx` were byte-identical. Each step folder now holds exactly the files the tutorial tells you to create. **2. React dedupe** (`4b865aabc`) `stream-chat-react` is consumed as a workspace dependency, so Vite serves its built output from outside the app's root and resolves that copy's `react` import separately from the app's. The SDK and the app end up on two React instances and the first hook call throws. `resolve.dedupe: ['react', 'react-dom']` forces both onto a single copy. Split out on its own because it is the one change that is not tutorial content β it can be cherry-picked or reverted independently. **3. Step stylesheet cleanup** (`6a67e4447`) β no rendering change The step browser renders every step in one document, so all seven stylesheets are live at once. Steps 3 and 4 import the SDK stylesheet *unlayered* (as the tutorial has them, since Step 5 is where you are taught to move it into a layer), and unlayered CSS outranks every `@layer` regardless of specificity β so the tutorial's `@layer stream-overrides { .custom-theme { ... } }` silently does nothing here. The themed steps declare the tokens unlayered on `.str-chat.custom-theme` instead. `Channel.tsx:163` and `ChannelList.tsx:342` both do `clsx('str-chat', theme, ...)`, so the theme class lands on the same element as `str-chat`; at 0,2,0 this beats the SDK's own `.str-chat` (0,1,0) regardless of source order, and it only matches steps that actually pass `theme="custom-theme"`, so it cannot leak into the unthemed ones. README flags this as the one deliberate deviation, with a pointer to keep the tokens in the layer in your own app. That leaves `layout.css` with exactly **two distinct versions**, mirroring the tutorial's two, byte-identical within each group so drift shows up in a diff. Also: `html` / `body` / `#root` were previously declared *only* in the step stylesheets, so the chrome silently depended on a step's CSS for `body { margin: 0 }` and would pick up the UA margin if steps were ever loaded lazily or in isolation (`2-client-setup` has no `layout.css` at all). `tutorial-main.css` now declares them itself. Parts of each `layout.css` copy are inert inside the step browser and stay that way on purpose, since the file has to remain a faithful copy of what the tutorial has readers write. README documents each case: - the `custom-theme` tokens do nothing in `7-emoji-picker` and `optional-livestream`, which do not pass `theme="custom-theme"` β matching the tutorial, where the reader's single `layout.css` holds the tokens and leaves them unused for those same two examples - the `.str-chat__*` widths lose to `.tutorial-browser__step-shell .str-chat__*` in `tutorial-main.css` (0,2,0 against 0,1,0); the tutorial's widths assume the app owns the whole page, here it is sized to fit a preview card No bundle cost either way β Vite collapses the identical copies, so the built CSS contains one `width: 30%` and one `@layer stream`. ### π¨ UI Changes No change to the SDK itself, and no change to any tutorial code block. The visual deltas are all in the example app: - **New `5-theming` step**, so the tutorial's theming milestone is now runnable. - **Preview panel no longer clips.** Before, the chat UI was cut off at the card's edges and the composer sat below the fold. Now every step reports `overflowX/Y: 0` with the preview card fully within the viewport. - **Step 2 corner padding.** `2-client-setup` renders bare text with no chat chrome, so it landed inside the card's 28px corner arc and the first glyph was clipped. Padded via the `step-client-setup` class. Verified across all eight steps by reading computed styles after switching: | Step | Theme class on `.str-chat` | `--str-chat__accent-primary` | | --- | --- | --- | | 3, 4 | `messaging light` | `#005fff` | | 5, 6, `optional-custom-attachment-type` | `custom-theme` | `#0d47a1` | | 7 | `messaging light` | `#005fff` | | `optional-livestream` | `str-chat__theme-dark` | `#4586ff` | Step 7 intentionally shows the default accent β the tutorial's emoji block renders `<Chat>` without the `theme` prop, and [notes so explicitly](GetStream/getstream.io#345). `tsc -b`, `vite build`, and gated prettier all pass. No console errors on any step. To see it: `yarn start:tutorial` from the repo root. --- Opened without a reviewer since I am not sure who owns this area β happy to add whoever should look at it. Companion PR: GetStream/getstream.io#345 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Expanded the tutorial browser with updated step structure, channel list, theming, custom UI components, emoji picker, and livestream (including optional milestones). * **Documentation** * Refreshed the tutorial README with current folder/step mapping and step-browser rendering notes. * **Bug Fixes** * Improved tutorial preview/layout sizing to avoid viewport clipping/overflow. * Prevented issues from multiple React instances during development. * **Refactor** * Reworked step mounting and applied consistent theme overrides across steps. * **Chores** * Updated the example environment key name to `VITE_STREAM_API_KEY` (with legacy fallback). <!-- end of auto-generated comment: release notes by coderabbit.ai -->
### π― Goal Adds ability to override icons through the component context. Ref: GetStream/stream-chat-react-native#3731 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added `useComponentContextIcons`, enabling centralized, context-based icon customization (with SDK defaults) across messages, attachments, dialogs, forms, media, polls, reactions, and channel views. * **Bug Fixes** * Improved icon override merging so nested `icons` customizations are preserved and combined correctly instead of being replaced. * **Documentation** * Expanded deprecation guidance for select icon customization props in favor of `ComponentContext` icon slots. * **Tests** * Updated test mocks to support component-context icon resolution. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
### π― Goal Ref: GetStream/stream-chat-react-native#3679 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved unread-count accuracy for channels using local unread tracking. * Marking messages as read now updates channel lists and delivery indicators consistently. * Read-state updates no longer incorrectly reset unread counts. * Hidden-tab unread indicators now stay synchronized with local unread activity. * Read actions continue to work when server read events are unavailable. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
### π― Goal `@emoji-mart/react` declares `react` as `^16.8 || ^17 || ^18` in its `peerDependencies` β React 19 is missing. Integrators on React 19 therefore hit peer-dependency resolution errors on install and have to add `package.json` overrides to get past them, even though the package works fine on React 19 in practice. The wrapper that package provides is ~20 lines of glue around the `emoji-mart` `Picker` custom element. Rather than asking every React 19 integrator to carry an override, we vendor it and drop the dependency. ### π Implementation details **Vendored the wrapper** - new `src/plugins/Emojis/Picker.tsx`, taken from [`@emoji-mart/react`](https://github.com/missive/emoji-mart/blob/16978d04a766eec6455e2e8bb21cd8dc0b3c7436/packages/emoji-mart-react/react.tsx) (MIT, Copyright (c) Missive). Behaviour is identical to upstream;
### π― Goal Agent-facing documentation in this repo was spread across three files that nothing verified, and all three had drifted. **`AI.md`** was a hand-maintained integration guide for AI assistants, shipped in the npm tarball. It duplicates what the official docs and tutorial already cover. Every item below is wrong on `master` today: | Claim in `AI.md` | Reality | | --- | --- | | `import 'stream-chat-react/dist/css/v2/index.css'` β **6 occurrences** | `build-styling` emits `dist/css/index.css`; there is no `dist/css/v2/`, so this import cannot resolve | | `examples/tutorial/src/4-custom-ui-components/` | `6-custom-ui-components` | | `examples/tutorial/src/7-livestream/` | `optional-livestream` | | `examples/tutorial/src/6-emoji-picker/` | `7-emoji-picker` | | `examples/vite/src/stream-imports-theme.scss` | does not exist (`examples/vite/src/index.scss`) | | `stream-chat`: `^9.27.2` | `^9.50.2` | | install `@emoji-mart/react`, add React 19 overrides | no longer a dependency (#3255) | An AI-facing guide that hands out a CSS import path which doesn't exist is worse than no guide β it produces confidently broken integrations. We now publish maintained [agent skills](https://getstream.io/agent-skills/docs/installation/) that cover integration properly and stay in sync across SDKs. **`CLAUDE.md` and `AGENTS.md`** covered overlapping ground with no shared source, so each drifted independently β `CLAUDE.md` still documented Jest, a Playwright e2e suite, and a `MessageInput` component, none of which exist. Two files describing one repo is the reason they were both wrong. **`yarn types`** silently checked nothing (details below), so the type errors a contributor expected it to catch went unreported. ### π Implementation details #### 1. Deleted `AI.md` 423 lines, and removed from the `files` array in `package.json` β it was being published to npm, so this drops a file from the package tarball, not just from the repo. **Added a `Build with AI Agents` section to `README.md`**, directly after *React Chat Tutorial*. The tutorial is presented as the best way to get started, so the agent-driven path belongs beside it rather than buried further down. It documents the install (`curl -fsSL https://getstream.io/cli.sh | bash` + `getstream init`), links [`/stream-react`](https://getstream.io/agent-skills/docs/skills/stream-react/), and shows example invocations. Three entry points, so it's discoverable however someone scans the README: a `Quick Links` bullet at the top, the section itself, and a cross-reference from the existing "Using AI assistants" block at the bottom β that block points at `AGENTS.md`, which is about *contributing to this repo*, a different audience from someone integrating the SDK. Content came from the live docs pages rather than memory, so the install command, the four skill capabilities (scaffold / enhance / audit / migrate, including Sendbird β Stream Chat) and the supported-agent list match what the docs actually say. #### 2. `AGENTS.md` is now the single source; `CLAUDE.md` imports it `CLAUDE.md` is reduced to a pointer ending in `@AGENTS.md`, which Claude Code expands inline. `AGENTS.md` absorbed the architecture content and keeps its own contribution rules, so there is one file to maintain for every agent that reads this repo β and `AGENTS.md` is already the filename Copilot, Cursor, Codex and Aider read. An import rather than a symlink: git symlinks degrade to a plain text file on Windows checkouts with `core.symlinks=false`, which would leave Claude Code with no guidance at all. Every claim in the merged file was re-derived from source rather than carried over. Corrections: | Was documented | Reality in `src` | | --- | --- | | "Run Jest tests", `yarn e2e`, `yarn e2e-fixtures` | Vitest only; no Jest, no Playwright suite in this repo | | `<MessageInput>` + `MessageInput/hooks/` | Directory no longer exists β it's `MessageComposer`, backed by `stream-chat`'s `MessageComposer` class | | `<Channel Message={CustomMessage} />` | `ChannelProps` carries no component slots; overrides go through `<WithComponents overrides={{ β¦ }}>` | | `useStateStore(chatClient.state.channelsArray)` | A selector is required; shallow-compares selected keys | | 3 bundle entry points | 4 β `channel-detail` was added; `build-styling` emits 4 stylesheets | | `css-reset β stream-new β β¦` layers | `modern-normalize, stream-new, stream-new-plugins, stream-overrides, stream-app-overrides` | | `_global-theme-variables.scss` | `variable-tokens.scss` + `light.scss`/`dark.scss` | | Yarn binary pinned to `yarn-4.14.1.cjs` | Now unpinned by filename (it's 4.15.0 and moves) | | "Never commit directly to `main`" | Default branch is `master` | | Styling / Build / i18n sections duplicated verbatim | Deduplicated | Added, because it isn't discoverable without reading several files: composer state ownership and the `client.messageComposerCache` resolution order; the Vite 8 / Rolldown constraints in `vite.config.ts` and why they must not be "simplified" (hardcoded `es`/`cjs` output dirs, regex externals for subpath imports); the `npmMinimalAgeGate: 1d` / `enableScripts: false` dependency gates; the Vitest setup contract and `mock-builders` inventory; `src/a11y` primitives; i18n `keySeparator: false` (keys legitimately contain `/`); and the CI job list. The architectural sections that still held were each re-verified against source before being kept: the 500ms/200ms/500ms-leading/2000ms throttles in `Channel.tsx`, `PREPEND_OFFSET = 10 ** 7`, the `processMessages` ordering, the string-serialization memoization FIXME in `useCreateChannelStateContext`, `areMessageUIPropsEqual`'s cheap-prop ordering, and the `react-compat` ESLint block. #### 3. `yarn types` now type-checks `src` ```diff - "types": "tsc --emitDeclarationOnly false --noEmit", + "types": "tsc --project tsconfig.lib.json --noEmit", ``` Without `--project`, `tsc` resolves the root `tsconfig.json` β a solution-style config with `"files": []` and project references only. It therefore checked **no files**, exited in under a second and always passed. `--emitDeclarationOnly false` is dropped: it only existed to dodge the old TS5053 error when `--noEmit` met `emitDeclarationOnly`, and under this repo's TypeScript 6 the output is byte-identical without it (diffed both forms). `yarn types:tests` is left alone but is now documented honestly: it is **not run by CI** and is currently red repo-wide (~1300 errors), so `AGENTS.md` frames it as advisory against a baseline rather than a green gate. Worth a follow-up; out of scope here. ### β Verification - `yarn lint` β exit 0 (prettier covers Markdown in this repo) - **`yarn types` was verified to actually check, not merely to run**: injected `const __typecheck_probe: number = "not a number"` into `src/utils/getChannel.ts`, confirmed it was reported as `TS2322`, then reverted. Before this change the same probe produced no output. - Nothing in CI invokes `yarn types`, so enabling it cannot turn CI red. The same `tsconfig.lib.json` is already compiled by `yarn build` in CI with `noEmitOnError`, so `src` type errors were failing CI before this change too β this only makes the check runnable locally under a memorable name. - `require()` of `package.json` to confirm the earlier `files` edit kept it valid; no remaining references to `AI.md` anywhere in the repo. ### π¨ UI Changes None β documentation and one script definition. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Added an βAI Agent Skillsβ quick link and guidance for building, upgrading, integrating, auditing, and migrating Stream Chat React applications with AI agents. * Replaced outdated repository guidance with expanded documentation covering development workflows, architecture, testing, accessibility, styling, troubleshooting, and contribution practices. * Removed the obsolete AI integration guide and related references. * Updated published package contents to reflect the documentation changes. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
β¦cted channels (#3257) ### π― Goal Fixes: #3254 `ChannelInner` called `channel.getConfig()` directly in the component body. That call throws `You can't use a channel after client.disconnect() was called` once the channel is disconnected β which happens when the current user is removed from a channel or the channel is deleted. The flag is flipped by an async WS event while `<Channel>` is still mounted, so the throw landed in the render phase and tore down the surrounding subtree. Same failure class as #2393 and #3248. ### π Implementation details Added an internal `getChannelConfig(channel)` helper that returns `undefined` for a disconnected channel instead of calling `getConfig()`, and applied it everywhere the config was read during render or in an effect: - `Channel.tsx` β the reported crash. Now also a lazy `useState` initializer, so the call no longer re-runs on every render. - `AttachmentSelector.tsx` and `useMessageComposerCommands.ts` - `useMarkRead.ts` `handleEvent` in `Channel.tsx` also early-returns for a disconnected channel, and the composer skips draft creation on unmount. `loadMoreNewer` picked up the `channel.disconnected` guard that `loadMore` already had β without it, scrolling to the bottom of a disconnected channel still queried a dead channel on every attempt (caught by the existing `try`/`catch`, so only log noise and a redundant dispatch). Fixing `Channel` alone is not enough: the crash relocates to `AttachmentSelector` once `ChannelInner` stops throwing and its subtree starts rendering. `undefined` is already part of `getConfig()`'s return type, so degradation is graceful β no read events, no commands, and the attachment selector renders nothing instead of crashing. > **Overlaps with #3249**, which adds the same `if (messageComposer.channel.disconnected) return;` line along with a more complete treatment of that effect (`.catch()` on `createDraft()`, a drafts-enabled check, and `preventClearingOnUnmount`). #3249 should own that effect β the line is kept here only so this PR stays independently mergeable. Whoever merges second should drop the duplicate. 9 tests added, each verified to fail against the unfixed code first. ### π¨ UI Changes None. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Improved stability when channels disconnect. - Prevented pagination, event handling, read receipts, attachment actions, and composer cleanup from triggering errors after disconnection. - Preserved channel configuration safely across re-renders and user deletion scenarios. - **Tests** - Added regression coverage for disconnected-channel behavior across messaging, composer, pagination, and read-state features. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
### π― Goal Fixes: #3248 Closes: REACT-1046 As a side feauture, adds `preventClearingOnUnmount` prop. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added inline message editing with an βEdit inlineβ action, prefilled composer, and cancel support. * Added support for preserving message composer content when it is removed from the screen. * Added flexible composer controller access for customized composition flows. * Improved composer selection across channels, threads, and parent messages. * **Bug Fixes** * Prevented unnecessary cleanup for disconnected channels. * Improved draft handling and composer state preservation during navigation and remounting. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## [14.11.0](v14.10.0...v14.11.0) (2026-08-07) ### Bug Fixes * **Channel:** guard render-phase channel.getConfig() against disconnected channels ([#3257](#3257)) ([f60273f](f60273f)), closes [#3254](#3254) [#2393](#2393) [#3249](#3249) * **EmojiPicker:** drop @emoji-mart/react peer dependency ([#3255](#3255)) ([0820e4c](0820e4c)) ### Features * add icons to ComponentContext ([#3246](#3246)) ([972b68c](972b68c)) * localized unread count ([#3250](#3250)) ([1b8fa34](1b8fa34)), closes [GetStream/stream-chat-react-native#3679](GetStream/stream-chat-react-native#3679) * **MessageComposer:** introduce context for custom composers ([#3249](#3249)) ([5776c16](5776c16)), closes [#3248](#3248)
Brings 14.11.0 into the v15 line: ComponentContext icon slots (useComponentContextIcons), the local unread count feature, the custom MessageComposer controller context (#3249), and the disconnected-channel guards (#3257). Conflict resolution kept v15's architecture (StateStore paginators, useChannel, WorkspaceNavigation) and layered master's work on top. Ported from master: - icons: conflicted components now read icons from useComponentContextIcons() instead of importing them statically, including the relocated src/plugins/SlotLayout/ChatView.tsx - local unread count (markReadLocally / isLocalUnreadCountEnabled / message.read_locally) reimplemented in v15 idioms in useMarkRead, Channel.handleEvent and ChannelListItem - #3257: v15's useChannelConfig reads from client.configsStore and never calls channel.getConfig(), so the render-phase crash is structurally gone. Kept the handleEvent disconnected early-return and AttachmentSelector's degrade-to-no-actions behaviour Superseded by v15 and dropped: - Channel's reducer / throttled copyStateFromChannelOnEvent machinery - useMessageDeliveryStatus's event listeners (now derived from the LLC's messageReceiptsTracker) - the inline navigation in MessageAlsoSentInChannelIndicator (now the useMessageAlsoSentInChannelNavigation hook) Also: - extracted MessageComposerControllerContext into its own module to break an import cycle introduced by #3249 meeting v15's barrel imports - CLAUDE.md is now just @AGENTS.md, so v15's architecture notes moved into AGENTS.md - master's Channel.test.tsx additions test ChannelStateContext and loadMore, both removed in v15, so the #3254 regression test for Channel is not carried over; the useMarkRead and AttachmentSelector #3254 tests are Verified on the merged tree: 2777 tests pass (230 files), tsc clean, eslint --max-warnings 0 clean, prettier clean.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the βοΈ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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 |
Codecov Reportβ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## release-v15 #3260 +/- ##
==============================================
Coverage ? 83.80%
==============================================
Files ? 527
Lines ? 16088
Branches ? 5063
==============================================
Hits ? 13483
Misses ? 2605
Partials ? 0 β View full report in Codecov by Harness. π New features to boost your workflow:
|
master's #3254 regression test asserted against ChannelStateContext and loadMore, both removed in v15, so it could not be carried through the merge. These replace it against the v15 architecture. - does not throw when re-rendering after the channel disconnects while mounted β the reported crash - does not read the config off the disconnected channel during render β locks the mechanism. useChannelConfig resolves from client.configsStore by cid, so the channel instance is never touched; the guarded getChannelConfig(channel) helper would satisfy this too, an unguarded channel.getConfig() would not - ignores events dispatched for a disconnected channel β covers the handleEvent early-return, since the user.deleted branch re-queries Each verified to fail against unfixed code first: reintroducing the render-phase channel.getConfig() fails the first two with the real "You can't use a channel after client.disconnect() was called", and removing the handleEvent guard fails the third with 2 unexpected query calls.
π― Goal
Merge
master(14.11.0) intorelease-v15. The two lines had diverged along different axes and 25 files conflicted, several of them non-mechanically β this PR exists so the resolution gets reviewed rather than landing straight onrelease-v15.mastermigrated every static icon import to the overridableuseComponentContextIcons()slot, and added the local unread count feature, the customMessageComposercontroller context (feat(MessageComposer): introduce context for custom composersΒ #3249) and the disconnected-channel guards (fix(Channel): guard render-phase channel.getConfig() against disconnected channelsΒ #3257).release-v15removedChannelStateContext/ChannelActionContextin favour ofuseChannel()+ StateStore paginators, relocatedChatViewtosrc/plugins/SlotLayout/, and renamed severalstream-chattypes.Those are mostly orthogonal, so the guiding rule was keep v15's structure, layer master's work on top.
π Implementation details
Ported from
masterinto v15 idiomsuseComponentContextIcons()instead of importing them statically. Also applied tosrc/plugins/SlotLayout/ChatView.tsx, the file that replaced the deletedsrc/components/ChatView/ChatView.tsx, so those icons stay overridable.markReadLocally/isLocalUnreadCountEnabled/message.read_locally) touched 5 files on master, 4 of them conflicted, and v15 had none of it. Reimplemented inuseMarkRead,Channel.handleEvent, andChannelListItem(using v15'sEventPayload+ subscription-object style).useChannelConfigreads fromclient.configsStoreand never callschannel.getConfig(), so the render-phase crash is structurally impossible and that part of the fix is superseded. Two effects still mattered and are kept: thechannel.disconnectedearly-return inChannel.handleEvent(v15 still callscountUnread()/muteStatus()/query()there), andAttachmentSelectordegrading to no actions when disconnected rather than offering actions that cannot succeed.Superseded by v15, so
master's side was droppedChannel's reducer and throttledcopyStateFromChannelOnEventmachinery.useMessageDeliveryStatus's event listeners β status is now derived from the LLC'smessageReceiptsTrackersnapshot store.MessageAlsoSentInChannelIndicatorβ now theuseMessageAlsoSentInChannelNavigationhook, with theonViewescape hatch.Fixes needed to make the merged tree work
useMessageComposerController β ../MessageComposer, an edge v15 did not have. Combined with v15's barrel imports it formed a cycle that leftuseMessageComposerControllerundefinedat render and broke 163 tests. Extracted the context intoMessageComposerControllerContext.tsxand re-exported it fromMessageComposer.tsx, so the public API is unchanged.PollOptionWithVotesHeaderhad been auto-resolved to master'sPollOptiontype; every other file undersrc/components/Poll/uses v15'sPollOptionResponseData, so it was switched.QuotedMessagePreviewusesIconNoSignfor deleted messages (v15-only) and master'sIconSetdid not cover it β threaded through the icon set so it stays overridable instead of remaining a static import.Docs
mastercollapsedCLAUDE.mdto@AGENTS.md, so v15's architecture notes had nowhere to live. Folded them intoAGENTS.md: context layers, state management, WebSocket event processing, memoization, and the thread/channel message invariants. Without this the merge would have silently restored v14 guidance.#3254 coverage
master'sChannel.test.tsxregression test asserted againstChannelStateContextandloadMore, both removed in v15, so it could not be carried through the merge. Replaced with three tests written against the v15 architecture:useChannelConfigresolves fromclient.configsStoreby cid so the channel instance is never touched; the guardedgetChannelConfig(channel)helper would satisfy this too, an unguardedchannel.getConfig()would not.handleEventearly-return, since theuser.deletedbranch re-queries the channel.Each was verified to fail against unfixed code first: reintroducing the render-phase
channel.getConfig()fails the first two with the realYou can't use a channel after client.disconnect() was called, and removing thehandleEventguard fails the third with 2 unexpectedquerycalls. TheuseMarkReadandAttachmentSelector#3254 tests from master also survive, adapted to v15's helpers.Verification β all run against the merged tree:
vitest runtsc -p tsconfig.lib.json --noEmiteslint src --max-warnings 0prettier --list-differentThe 163-test failure from the import cycle was confirmed as merge fallout, not pre-existing, by running the same specs against pre-merge
release-v15in a scratch worktree (they passed there).π¨ UI Changes
None intended β this is a merge with no new UI. The behavioural deltas are the ported local unread count and the disconnected-channel degradation described above.