Skip to content

feat: migrate components from ChannelActionContext and ChannelState context to dedicated StateStore instances and add layout control API - #3237

Draft
MartinCupela wants to merge 86 commits into
release-v15from
feat/message-paginator-master-merge
Draft

feat: migrate components from ChannelActionContext and ChannelState context to dedicated StateStore instances and add layout control API#3237
MartinCupela wants to merge 86 commits into
release-v15from
feat/message-paginator-master-merge

Conversation

@MartinCupela

Copy link
Copy Markdown
Contributor

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 reactive StateStore-driven sources (messagePaginator, configState, threads.state, etc.).

Scope

  • Introduces a new ChatView layout control API with slot-based entity binding and view-aware state.
  • Replaces ChannelActionContext and ChannelStateContext runtime usage with instance APIs from stream-chat-js.
  • Migrates message/thread interactions to messagePaginator and client instance APIs.
  • Adds React adapters/hooks for slot-resolved channel/thread rendering and navigation.
  • Reworks Channel/Thread request handler wiring to instance configState.requestHandlers.
  • Updates components/stories/tests across the SDK to use the new contracts.

What Changed

1) New ChatView Layout Control API

  • Added LayoutController state model for slot topology, bindings, visibility, and per-slot history.
  • Added ChatViewNavigation API:
    • openView(view, { slot? })
    • openChannel(channel, { slot? })
    • closeChannel({ slot? })
    • openThread(threadOrTarget, { slot? })
    • closeThread({ slot? })
    • hideChannelList({ slot? })
    • unhideChannelList({ slot? })
  • Added slot-oriented primitives:
    • ChannelSlot
    • ThreadSlot
    • ChannelListSlot
    • ThreadListSlot
    • useSlotEntity, useSlotChannel, useSlotThread
  • Added layout state serialization helpers:
    • serializeLayoutState
    • restoreLayoutState
  • Added resolver utilities in layoutSlotResolvers for deterministic slot targeting.

2) Channel/Thread Ownership Moved to stream-chat-js Instances

  • Channel/thread message state now comes from instance-level messagePaginator and thread manager state.
  • Channel and Thread flows are instance-driven and reactive via StateStore.
  • Thread open/close behavior is routed through useChatViewNavigation() (with legacy fallback deactivation in Thread).

3) Context Removal and Replacement

  • Removed runtime/public usage of:
    • ChannelActionContext
    • ChannelStateContext
    • TypingContext provider path from Channel runtime
  • Added ChannelInstanceContext + useChannel() as the channel resolution contract.
  • useChannel() resolves from:
    • active thread context first
    • otherwise ChannelInstanceContext

4) Message Pagination + Unread/Focus Migration

  • Added public hook:
    • useMessagePaginator()
  • Message list/thread operations now use:
    • messagePaginator.jumpToMessage(...)
    • messagePaginator.jumpToTheFirstUnreadMessage(...)
    • messagePaginator.jumpToTheLatestMessage()
    • messagePaginator.toHead()/toTail()
    • messagePaginator.ingestItem(...)
    • messagePaginator.removeItem(...)
    • messagePaginator.unreadStateSnapshot
  • Unread UI controls now use instance APIs and client.messageDeliveryReporter instead of context actions.

5) Request Handler Customization Moved to Instance Config

  • Channel and Thread now register custom request handlers into:
    • channel.configState.requestHandlers
    • thread.configState.requestHandlers
  • Covers custom send/update/delete/markRead paths without ChannelActionContext callbacks.

API Changes and Migration Guide

Navigation: setActiveChannel/context actions -> ChatView navigation

// Before
setActiveChannel(channel);
openThread(message);
closeThread();

// After
const { openChannel, openThread, closeThread } = useChatViewNavigation();
openChannel(channel);
openThread({ channel, message });
closeThread();

### Message list updates: context mutation helpers -> paginator reconciliation

```js
// Before
updateMessage(message);
removeMessage(message);

// After
const paginator = useMessagePaginator();
paginator.ingestItem(message);
paginator.removeItem({ item: message });

Message jump/pagination: context methods -> paginator methods

// Before
jumpToMessage(id);
jumpToFirstUnreadMessage();
loadMore();
loadMoreNewer();

// After
const paginator = useMessagePaginator();
paginator.jumpToMessage(id);
paginator.jumpToTheFirstUnreadMessage();
paginator.toTail();
paginator.toHead();

Channel access: ChannelState/Action context -> useChannel()

// Before
const { channel } = useChannelStateContext();

// After
const channel = useChannel();

Slot-based rendering (new recommended pattern)

<ChatView>
  <ChatView.Channels slots={['list', 'main', 'thread']}>
    <ChannelListSlot slot='list' />
    <ChannelSlot slot='main' />
    <ThreadSlot slot='thread' />
  </ChatView.Channels>
</ChatView>

Behavioral Notes

  • Active entity routing is now layout/slot-driven instead of ChatContext active-channel ownership.
  • Thread/channel can coexist as sibling slot entities.
  • Channel list “open on mount” flows now open via navigation API.
  • Unread and notification behavior is now aligned with instance stores/reporters.

Testing

  • Broad test migration to the new contracts across Channel, Thread, MessageActions, MessageList, ChatView -navigation/layout, and slot hooks/components.
  • Added focused tests for:
  • useChannelRequestHandlers
  • useThreadRequestHandlers
  • ChatViewNavigation
  • layout controller behavior
  • slot resolution helpers

Breaking/Important for Integrators

  • Stop relying on ChannelActionContext and ChannelStateContext APIs.
  • Use useChatViewNavigation() for open/close channel/thread flows.
  • Use useMessagePaginator() + instance APIs for list mutations/jump/pagination.
  • Use slot adapters (ChannelSlot, ThreadSlot, list slots) for deterministic multi-pane ChatView layouts.
  • Prefer instance-scoped request handler overrides (Channel/Thread props wired to configState.requestHandlers).

🎨 UI Changes

No planned UI changes

# Conflicts:
#	src/components/MessageList/MessageList.tsx
#	src/components/MessageList/renderMessages.tsx
…rops) and Header Toggle Wiring for Entity List Pane
… and Remove Entity Semantics from LayoutController (Slot-Only Controller)
…read-on-mount, search-focused jump for Channel and rewrite Channel tests
oliverlaz and others added 30 commits July 14, 2026 11:29
…outs (#3241)

### 🎯 Goal

The message composer `<textarea>` does not fill its container when the
SDK is integrated into a custom composer layout — it collapses to ~160px
— even though it stretches correctly in our own example apps.

Root cause: the rules that give the composer textarea its width live
only inside the `.str-chat__message-composer-controls` wrapper, scoped
as:

```scss
.str-chat .str-chat__message-composer-controls .str-chat__textarea textarea { width: 100%; }
```

`.str-chat__textarea` and its `<textarea>` are rendered by
`TextareaComposer`, but a custom composer that renders
`<TextareaComposer>` without the default `MessageComposerUI` never
produces the `.str-chat__message-composer-controls` wrapper. So the
selector never matches, no `width` is applied, and the bare `<textarea>`
falls back to its intrinsic `cols=20` width (~160px). Our example apps
use the default `MessageComposerUI`, so the wrapper is present and the
bug is invisible there.

This is a recurring integration pain point, hence fixing it in the SDK
rather than asking each integrator to re-add the width rule.

### 🛠 Implementation details

Lifted the `.str-chat__textarea` block out of the
`.str-chat__message-composer-controls` wrapper in `MessageComposer.scss`
so it is scoped to the `TextareaComposer`'s own elements (`.str-chat
.str-chat__textarea` / `.str-chat .str-chat__textarea textarea`) instead
of the composer-controls layout wrapper.

Why this is safe:

- `.str-chat__textarea` and its `<textarea>` are rendered **only** by
`TextareaComposer`, and no other rule targets `.str-chat__textarea`, so
lowering selector specificity from `(0,3,1)` to `(0,2,1)` conflicts with
nothing.
- `flex: 1` on the wrapper is ignored when its parent isn't a flex
container (custom composers) and continues to work in the default
composer where the parent is `display: flex`.
- The default `MessageComposerUI` already had all of these declarations
applied, so its rendering is unchanged.

Net effect: any composer that renders `<TextareaComposer>` now gets a
full-width textarea, with or without the default `MessageComposerUI`.

### 🎨 UI Changes

Verified live in a custom (Slack-style) composer that renders
`<TextareaComposer>` without `MessageComposerUI`:

- **Before:** composer `<textarea>` computed width = **160px**
(intrinsic `cols=20` fallback).
- **After:** composer `<textarea>` computed width = **1059px** — fills
the composer container; the placeholder spans the full width.

No visual change in the default composer (`MessageComposerUI`), which
already carried these styles.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved message composer layout consistency by ensuring the text area
consistently fills available space across different composer
configurations.
* Preserved the existing text area look and behavior, including
typography, sizing, scrollbar handling, and focus-visible styling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
…rewire consumers

Add a slot-agnostic, intent-level WorkspaceNavigation context in core
(openChannel/openThread/closeThread, isChannelActive/isThreadActive,
openChannels/openThreads, isThreadsView/isThreadDismissable) with an inert
no-op default. The in-core ChatView implements it (workspaceNavigationAdapter)
and provides it to its subtree, so core components no longer depend on the
ChatView slot API directly.

Rewire the 12 core consumers (Message reply/thread handlers, Thread/ThreadHeader,
ChannelList/ChannelListItem/Search, Threads/ThreadList, Notifications target) onto
the adapter, and migrate their tests. First step of extracting the slot system
into an opt-in plugin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Relocate src/components/ChatView and the four slot bridges (ChannelSlot,
ThreadSlot, ThreadSlotContext, ThreadListSlot) into src/plugins/SlotLayout.
Fix intra-plugin imports and repoint sibling-component references to
../../components. The plugin index re-exports the bridges + layout as public API.

Drop the slot exports from the core component barrels; the ChannelDetail plugin
and the two ChatView-referencing tests now import from the SlotLayout path, and
the master SCSS @use is repointed. yarn types clean; full suite green.

BREAKING CHANGE: the ChatView slot system is no longer exported from the
stream-chat-react root; it will be published under the slot-layout subpath.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the `slot-layout` vite library entry (src/plugins/SlotLayout/index.tsx) and
the matching package.json `exports["./slot-layout"]` + typesVersions mapping,
mirroring the sibling slot-geometry plugin. Build emits dist/es/slot-layout.mjs,
dist/cjs/slot-layout.js and dist/types/plugins/SlotLayout/index.d.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add unit tests for the WorkspaceNavigation context: the inert no-op default
(empty reads, false predicates, non-throwing operations) and delegation to a
provided implementation. The ChatView/slot test suites already moved with the
plugin in the SlotLayout relocation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ot-layout

The ChatView slot system moved to the slot-layout plugin subpath, so repoint the
vite + tutorial example imports (ChatView, Slot bridges, slot hooks, layout types)
from the stream-chat-react root to stream-chat-react/slot-layout. Example type-checks
clean. (Sync.tsx carries an unrelated in-progress change and is migrated separately.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…API (#3242)

## Summary

Several components, hooks, and contexts are used internally and
referenced by the docs, but were never re-exported from the package
barrels — so consumers can't import them. This wires them into the
public API. Purely additive barrel re-exports; no behavior change.

## Newly exported

**`src/context/index.ts`**
- `VirtualizedMessageListContext`,
`VirtualizedMessageListContextProvider`,
`useVirtualizedMessageListContext`, `VirtualizedMessageListContextValue`

**`src/components/index.ts`**
- `SummarizedMessagePreview`, `useLatestMessagePreview` (+ its
`ChannelPreviewMessageType`, `ChannelPreviewDeliveryStatus`,
`LatestMessagePreviewData`, `UseLatestMessagePreviewParams` types)

**`src/components/MessageComposer/index.ts`**
- `VoiceRecordingPreviewSlot` (component; only its Props type was
exported before)
- `MessageComposerActions` and `AdditionalMessageComposerActions`

## Why

A docs audit of the v14 React SDK found multiple cookbook/reference
pages instructing readers to import these symbols from
`stream-chat-react`, which fails at runtime because the barrels don't
re-export them. Rather than route the docs around them, expose the
symbols (the docs update is in GetStream/docs-content#1431).

## Notes
- No collisions: none of the newly-exported names are exported
elsewhere.
- `AdditionalMessageComposerActions` and the four
`useLatestMessagePreview` types ride along via `export *` — intentional;
flag if you'd prefer narrower named exports.
- Not exposed (left internal, unchanged): `EditedMessagePreview`,
`SendToChannelCheckbox`, `restorePreEditSnapshot`.

## Checks
- `yarn types` ✅
- `yarn eslint` (changed files) ✅
- Barrel-only change; no tests added. Happy to add export-surface tests
if desired.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
  * Added support for voice recording previews in the message composer.
  * Exposed additional message composer actions.
  * Added summarized message preview functionality.
  * Added access to virtualized message list context functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
### 🎯 Goal

Ref: https://getstream.slack.com/archives/C02R5UCGN6N/p1784023573649669
Relies on: GetStream/stream-chat-js#1800


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved file-upload availability by basing upload enablement on the
message composer’s configured file limits.
* Made the multi-action attachment UI consistently display the file
upload option, even when channel capability details are unavailable.
* Updated upload control behavior to better reflect current attachment
slot availability and upload-request handling.
* **Chores**
* Updated the underlying `stream-chat` dependency version (main and
example apps).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## [14.9.0](v14.8.0...v14.9.0) (2026-07-16)

### Bug Fixes

* custom doUploadRequest permission adjustments ([#3243](#3243)) ([fa67adf](fa67adf)), closes [GetStream/stream-chat-js#1800](GetStream/stream-chat-js#1800)
* **MessageComposer:** make textarea full-width in custom composer layouts ([#3241](#3241)) ([05236db](05236db))

### Features

* expose internal contexts and composer components in the public API ([#3242](#3242)) ([6b6a828](6b6a828)), closes [GetStream/docs-content#1431](https://github.com/GetStream/docs-content/issues/1431)
Checkpoint before the stream-chat storage removal (ChannelState message-list + threads).

Migrates ChannelListItem, Channel, MessageList, Search, MessageBounce + mock-builders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "should allow removing messages" test still read the removed channel.state main
message list via findMessage/removeMessage. Re-scope it to channel.messagePaginator:
seed check via items, remove via removeItem, and assert the item leaves the active window.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nedMessagesPaginator

usePinnedMessagesCount now derives the count reactively from channel.pinnedMessagesPaginator via
useStateStore instead of reading channel.state.pinnedMessages and subscribing to channel events.
Tests drive a real paginator StateStore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rs to paginators

Two readers still referenced state removed from stream-chat: Channel.tsx read
channel.state.messages (the main list moved to channel.messagePaginator) for the oldest-message
id on user.deleted, and ThreadListItemUI read ThreadState.replies (thread replies moved to
thread.messagePaginator) for the latest reply. Point both at the paginators — the latest reply is
now selected reactively from thread.messagePaginator.state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
….latestMessage

Replace the `headItems?.at(-1)` selector with the paginator's tracked `latestMessage` (resolved
via `latestMessageId` + `getItem`), so the latest reply is correct regardless of the active window
or the interval/item sort orientation rather than assuming the head window's last entry is newest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ator model

ChannelState message/thread/pinned storage was removed on the LLC side (v15): the paginators
(channel.messagePaginator, thread.messagePaginator, channel.pinnedMessagesPaginator) are the source
of truth. Update the stale guidance accordingly:

- DO-NOT list: there is no channel.state.addMessageSorted() / removeMessage() (removed); read via
  useStateStore(channel.messagePaginator.state, ...). Thread replies are an independent paginator,
  not mirrored into the channel list.
- Replace the "Thread State Synchronization" invariant ("replies MUST exist in main channel state")
  with the independent-paginators model; show_in_channel governs channel visibility.
- Optimistic-updates gotcha and the context-deps example no longer reference channel.state.messages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The reducer-era state management is gone from src (channelState.ts, makeChannelReducer,
useReducer, copyStateFromChannelOnEvent, useCreateChannelStateContext, and the
ChannelStateContext / ChannelActionContext layers). Rewrite the stale sections:

- Context Layers: drop ChannelStateContext / ChannelActionContext; add ChannelInstanceContext
  + useChannel() / useMessagePaginator(); note the removed context/hook builders.
- State Management: no reducer; Channel holds only UI flags (isBootstrapping / bootstrapError);
  message state lives on the LLC paginators, consumed via useStateStore (useSyncExternalStore).
- Optimistic Updates: handled by the LLC (channel.sendMessage -> messagePaginator.ingestItem),
  not React state; conflict resolution is in the paginator.
- WebSocket Event Processing: no throttled copyStateFromChannelOnEvent; handleEvent does side
  effects only; re-renders come from StateStore subscriptions.
- Performance: memoization via useStateStore selectors + areMessageUIPropsEqual; the old
  throttles and string-serialization memoization are gone.
- Module Boundaries: update the coupling/risk notes accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…3244)

### 🎯 Goal

Unify how the Avatar/AvatarStack information is being constructed
through the common and replaceable utility `extractDisplayInfo`
function.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Added an `initials` prop override for avatars.
* Introduced a context-overridable display-info extractor to customize
avatar rendering across messages, threads, polls, reactions, search
results, typing indicators, and channel/member/pinned views.
* **Bug Fixes**
* Improved consistency for avatar display data by trimming usernames and
using the trimmed name for avatar `alt` text, with the explicit
`initials` override taking priority.
* **Tests**
* Updated component-context mocks to support the new context-driven
avatar customization.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## [14.10.0](v14.9.0...v14.10.0) (2026-07-22)

### Features

* introduce extractDisplayInfo for Avatar/AvatarStack components ([#3244](#3244)) ([1068e79](1068e79))
Reconcile PR #3245 (unread indicators) onto feat/message-paginator-master-merge
and fix duplicate mark-read on channel open.

Channel.tsx:
- useMarkRead is now the sole mark-read owner on open. Removed the redundant
  mount-time markChannelRead and the markReadOnMount prop; a second, unthrottled
  mark-read used to fire alongside useMarkRead (up to 4 /read requests per open
  under StrictMode, 2 in production) — now a single request.
- Guard seedUnreadSnapshot() so it does not run when the channel is already
  flagged unread (firstUnreadMessageId set); re-seeding would clear a deliberate
  "mark as unread" on reopen.

useMarkRead.ts:
- shouldMarkRead now gates on messagePaginator.isViewingLive (tab foregrounded
  AND scrolled to the bottom AND no newer messages beyond the loaded window) for
  the active collection. Adds the "not caught up while newer messages exist"
  guard and removes the dead scrolled-back-to-bottom tracking.

MessageList.tsx / VirtualizedMessageList.tsx:
- Wire hasMoreNewer into useMarkRead (the non-superseded half of PR #3245's
  message-list changes; the focus-signal hunks were dropped — the branch already
  implements message-focus clearing).

UnreadMessagesNotification.tsx:
- clearUnreadSnapshot() on the mark-read button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reconcile release-v15 (OpenAPI-era master, incl. the a11y overhaul) with the message-paginator architecture. Conflicts resolved to keep our paginator/MessageComposer/workspace-navigation model while adopting release-v15's improvements, adapted where the APIs diverged:

- a11y: AriaLive announcements via Chat's shared outlet (no per-list AriaLiveRegion); ChannelList keyboard-nav listbox, now per-list on each scroll container; composed accessible labels + interaction announcements for channel/thread rows; ChatView navigation-landmark model.
- TypingIndicator ported to the state-store layer and wired into MessageList/VirtualizedMessageList (isMessageListScrolledToBottom/scrollToBottom).
- extractDisplayInfo/AvatarStack, doUploadRequest capability, exposed internal contexts.
- ChatView context: dropped the redundant activeChatView alias in favour of activeView.

Known-pending (tracked separately): 211 OpenAPI Event-union type adaptations and the mock-builder _setupConnection breakage. Committed with --no-verify.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants