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 76 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 76 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
@MartinCupela
MartinCupela changed the base branch from release-v10 to release-v15 July 10, 2026 11:36
MartinCupela and others added 28 commits July 14, 2026 10:02
Reset the composer optimistically before awaiting sendMessageWithLocalUpdate, matching
master's ordering. Clearing only after the network round-trip let a fast follow-up keystroke
or an async textComposer.handleChange commit race the clear, dropping or failing to clear
rapidly typed-and-sent messages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MessageComposer now owns the submission flow (compose() -> channel.sendMessageWithLocalUpdate()).

BREAKING CHANGE: the MessageComposer overrideSubmitHandler prop is removed. Intercept the
outgoing request via Channel's doSendMessageRequest, or transform the composed message with
composer composition middleware.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
useMuteHandler no longer gates the mute/unmute call on an optional notify callback (Message.tsx
wires it as useMuteHandler(message)), so muting is no longer a silent no-op. useMarkUnreadHandler
now propagates request failures instead of swallowing them, so the MarkUnread control reports the
error via useNotificationApi.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restore smooth (reduced-motion-aware) programmatic scrolling on the reactive message paginator,
and re-center a focused message when the list is revealed or resized (e.g. a thread panel
uncovering the channel) so jump-to-message lands correctly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Channel already defaults EmptyPlaceholder to null and renders it; widen the prop type to
React.ReactElement | null to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the deleted ChannelStateContext/ChannelActionContext shims and their mocks; read state from
channel.messagePaginator / channel.state and drive writes through channel.*WithLocalUpdate; mock
requests via the mock-builder API helpers; instantiate client and channel per test via a setup()
helper; remove obsolete cases and type-clean the suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seed ai-migration-v14-v15.md with the confirmed v15 breaking changes: ChannelStateContext and
ChannelActionContext removal, overrideSubmitHandler removal, and setActiveChannel /
channelsQueryState removal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-point the remaining tests onto the v15 architecture (low-level client / message paginator,
notification API, ChatView slots) and complete the JS-to-TS migration. Add an error code to the
erroredApi mock-builder helper used by the request-failure cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…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>
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>
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.

1 participant