Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 127 additions & 58 deletions ai-docs/ai-migration-v14-v15.md

Large diffs are not rendered by default.

39 changes: 33 additions & 6 deletions ai-docs/v15-breaking-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ yarn api-surface check --doc ai-docs/v15-breaking-changes.md --diff diff.json

## Index

100 entries.
101 entries.

| ID | Title | Group |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
Expand Down Expand Up @@ -165,6 +165,7 @@ yarn api-surface check --doc ai-docs/v15-breaking-changes.md --diff diff.json
| [BC-095](#bc-095) | `disableQuotedMessages` is removed from `MessageProps`, `MessageListProps` and `useMessagePermissions` | Messages and message lists |
| [BC-097](#bc-097) | channel detail views render no close button outside a modal without `onClose`; `ChannelDetail` calls `onLayoutChange` | Chat, ChatView and layout |
| [BC-098](#bc-098) | the channel members header action set uses `Component`, `placement: 'dropdown'`, `isVisible` and a `quick-dropdown-toggle` item | Contexts and customization |
| [BC-099](#bc-099) | `ComponentContext.HeaderEndContent` was removed; `ChannelListHeader` and `ThreadListHeader` take `StartContent` / `EndContent` props, rendered whenever set | Headers, threads and search |
| [BC-100](#bc-100) | `typing.twoUsers` takes the two names as `firstUser` and `secondUser` instead of one joined `typing` value | i18n |
| [BC-101](#bc-101) | `Root`, `Header`, `UploadIcon`, `BinIcon`, `PauseIcon`, `PlayIcon` unexported; `@stream-io/transliterate` dropped; `CustomChannelData` typed for apps; `DefaultChannelData` holds only `name` and `image` | Package and runtime |
| [BC-102](#bc-102) | `initialMessage` is removed from `MessageProps` and `MessageContextValue`; `useIsRenderedAsThreadParent()` tells the thread's parent message apart | Messages and message lists |
Expand Down Expand Up @@ -509,11 +510,11 @@ export const TypingCount = () => {
- Replacement:
- `Message` -> the `MessageUI` slot. `VirtualMessage` still overrides inside `VirtualizedMessageList` only. The `Message` _props_ on the lists and `Thread` belong to the message area's entry.
- `ThreadHeader` -> render `<ThreadHeader />` (or your own header) as a child of `<Thread thread={thread}>`. `Thread` renders children and no header of its own.
- `HeaderStartContent` -> the `StartContent` prop on `ChannelHeader` / `ThreadHeader`, which defaults to `WorkspaceNavigationBackButton`. `EndContent` defaults to `WorkspaceNavigationCloseButton`. `HeaderEndContent` stays on `ComponentContext` for the list headers.
- `HeaderStartContent` -> the `StartContent` prop on `ChannelHeader` / `ThreadHeader`, which defaults to `WorkspaceNavigationBackButton`. `EndContent` defaults to `WorkspaceNavigationCloseButton`. `HeaderEndContent` -> the `EndContent` prop on `ChannelListHeader` / `ThreadListHeader` (BC-099).
- `ChannelListUI` -> no wrapper slot. `ChannelList` renders an `InfiniteScrollPaginator` and reads the new slots `EmptyListIndicator`, `EndReachedIndicator`, `FirstPageLoadingIndicator`, `ListItem` and `LoadingNextPageIndicator` from `ComponentContext`. `ThreadList` reads `FirstPageLoadingIndicator` too, and both read the new `ListLoadErrorIndicator`; only `ChannelList` reads the others today.
- Evidence:
- `release-v14:src/context/ComponentContext.tsx:129` `ChannelListUI?`, `:179` `Message?` (`@deprecated use MessageUI`), `:288` `ThreadHeader?`, `:305` `HeaderStartContent?`
- `master:src/context/ComponentContext.tsx` none of the four; `:187` `MessageUI?`, `:294` `ThreadHead?`, `:309` `HeaderEndContent?`, `:317` `VirtualMessage?`, `:88-102` the new list slots
- `master:src/context/ComponentContext.tsx` none of the four; `:187` `MessageUI?`, `:294` `ThreadHead?`, `:317` `VirtualMessage?`, `:88-102` the new list slots
- `master:src/components/ChannelHeader/ChannelHeader.tsx:58,64,77,79` and `master:src/components/Thread/ThreadHeader.tsx:85,91,97,99` `StartContent` / `EndContent` props with the workspace-navigation defaults
- `master:src/components/Thread/Thread.tsx:41-56` JSDoc: `<Thread thread={thread}><ThreadHeader />…</Thread>`
- `master:src/components/ChannelList/ChannelList.tsx:48-54` reads `EmptyListIndicator`, `EndReachedIndicator`, `FirstPageLoadingIndicator`, `ListItem`, `LoadingNextPageIndicator` from `useComponentContext()`
Expand Down Expand Up @@ -1060,7 +1061,7 @@ A threads view is declared the same way (a `{ id: 'threads', slots: [...] }` lay
- Area: navigation
- User impact:
- compile error reading `setActiveChannel` / `channel` from `useChatContext()` (and from the return of `useChat()`); `undefined` in JS.
- silent behavior change: without `ChatView` (or your own `WorkspaceNavigationProvider`), clicking a `ChannelList` row, a channel/message/user search result, a `ThreadList` row, a message's reply-count button, "also sent in channel", or the channel-detail "send direct message" / pinned-message entry does nothing, and no list row renders as active (`ChannelListItem` derives `active` from `isChannelActive(cid)`, `ChannelListHeader` only shows `HeaderEndContent` while `openChannels` is non-empty).
- silent behavior change: without `ChatView` (or your own `WorkspaceNavigationProvider`), clicking a `ChannelList` row, a channel/message/user search result, a `ThreadList` row, a message's reply-count button, "also sent in channel", or the channel-detail "send direct message" / pinned-message entry does nothing, and no list row renders as active (`ChannelListItem` derives `active` from `isChannelActive(cid)`).
- with no navigation provided at all, each operation (`openChannel`, `openThread`, `closeChannel`, `closeThread`, `goBack`) logs a one-time console warning that nothing happened; `defaultWorkspaceNavigation` itself stays silent, so a provider built by spreading it does not warn.
- `ChannelListItem` / `ChannelListItemUI` no longer accept `setActiveChannel` / `watchers` (BC-033).
- Old API:
Expand Down Expand Up @@ -1998,10 +1999,10 @@ Guide check: `ai-migration-v14-v15.md` "`ChannelListItem` `getLatestMessagePrevi
- User impact: compile error on a `HeaderStartContent` override (`TS2561`); in JS it is ignored and the custom content (often a sidebar toggle) disappears. Visible change: by default `ChannelHeader` / `ThreadHeader` render `WorkspaceNavigationBackButton` at the start and `WorkspaceNavigationCloseButton` at the end; both render nothing outside a workspace navigation that reports the panel as stacked / dismissable.
- Old API:
- `ComponentContextValue.HeaderStartContent`
- Replacement: `<ChannelHeader StartContent={...} EndContent={...} />` and the same on `ThreadHeader`. Passing a component replaces the default button; render `WorkspaceNavigationBackButton` / `WorkspaceNavigationCloseButton` (new exports) inside yours to keep it. `ComponentContext.HeaderEndContent` is unchanged and still feeds `ChannelListHeader` / `ThreadListHeader` (but `ChannelListHeader` shows it only while a channel is open in the workspace).
- Replacement: `<ChannelHeader StartContent={...} EndContent={...} />` and the same on `ThreadHeader`. Passing a component replaces the default button; render `WorkspaceNavigationBackButton` / `WorkspaceNavigationCloseButton` (new exports) inside yours to keep it. `ComponentContext.HeaderEndContent` was removed too; the list headers take `StartContent` / `EndContent` props (BC-099).
- Evidence:
- `release-v14:src/context/ComponentContext.tsx:305`; `release-v14:src/components/ChannelHeader/ChannelHeader.tsx:52,61-63`
- `master:src/components/ChannelHeader/ChannelHeader.tsx:50-69,74-110`; `master:src/components/Thread/ThreadHeader.tsx:79-100`; `master:src/components/ChannelList/ChannelListHeader.tsx` (`hasActiveChannel = useWorkspaceNavigation().openChannels.length > 0`)
- `master:src/components/ChannelHeader/ChannelHeader.tsx:50-69,74-110`; `master:src/components/Thread/ThreadHeader.tsx:79-100`
- commit: `66557d63b feat!: port the tutorial alignment (#3251) to v15, with panel-aware workspace navigation` (PR #3301)
- Docs impact:
- `02-ui-components/04-channel/02-channel_header.md`, `02-ui-components/04-channel/05-component_context.md`, `02-ui-components/03-chat/01-chat.md`, `03-ui-cookbook/12-collapsible-sidebar.md`, `06-release-guides/01-upgrade-to-v14.md`
Expand Down Expand Up @@ -2247,6 +2248,32 @@ const CustomThreadHeader = () => {
- `03-ui-cookbook/07-notifications/01-system-notifications.md`, `04-guides/13-notifications.md`
- Example needed: no

<a id="bc-099"></a>

### BC-099: `ComponentContext.HeaderEndContent` was removed; `ChannelListHeader` and `ThreadListHeader` take `StartContent` / `EndContent` props, rendered whenever set

- Status: confirmed
- Area: channel list header / thread list header / ComponentContext
- User impact:
- compile error on a `HeaderEndContent` key written inline in `overrides` (`TS2353` / `TS2561`). In JS, or in an `overrides` object declared as a variable that also holds a valid key, the key is ignored without an error and the content disappears from both list headers.
- behavior change: v14 rendered `HeaderEndContent` in `ChannelListHeader` only while a channel was active, and in `ThreadListHeader` only while a thread was active. The `EndContent` prop renders whenever it is set, so content that only makes sense next to an open channel or thread (a sidebar toggle) has to return `null` itself.
- one component no longer fills both headers: pass it to each header that should show it.
- Old API:
- `ComponentContextValue.HeaderEndContent`, rendered by `ChannelListHeader` (gated on the active channel) and `ThreadListHeader` (gated on the active thread)
- Replacement:
- `ChannelListHeader` and `ThreadListHeader` take `StartContent` (before the title) and `EndContent` (after the title), as `ChannelHeader` / `ThreadHeader` do. Neither has a default. Types: `ChannelListHeaderProps`, `ThreadListHeaderProps`.
- where the SDK renders the header, the props go through the header's override key: register a component that renders the default header with them. `ChannelListHeader` is read by `ChannelNavigation`; `ThreadListHeader` is a new key, read by `ThreadList`. `ThreadListHeader` is now exported.
- where the app renders the header itself (for example `ChannelListHeader` above a standalone `ChannelList`), it passes the props directly; the override key does not reach that header.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
- to keep the v14 gate, read `useWorkspaceNavigation().openChannels` / `.openThreads` in the content component and return `null` while it is empty. `ChatView` or an app's own `WorkspaceNavigationProvider` fills these lists; without one they stay empty, so an app without either gates on its own selection state.
- Evidence:
- `release-v14:src/components/ChannelList/ChannelListHeader.tsx:15` `{channel && HeaderEndContent && <HeaderEndContent />}`; `release-v14:src/components/Threads/ThreadList/ThreadListHeader.tsx:12` `{activeThread && HeaderEndContent && <HeaderEndContent />}`; `release-v14:src/context/ComponentContext.tsx:303`
- `master:src/components/ChannelList/ChannelListHeader.tsx` and `master:src/components/Threads/ThreadList/ThreadListHeader.tsx` (`StartContent` / `EndContent` props); `master:src/components/Threads/ThreadList/ThreadList.tsx` reads `ThreadListHeader` from `useComponentContext()`; `master:src/context/ComponentContext.tsx` `ThreadListHeader?`
- ticket: REACT-1449
- examples: `examples/vite/src/ChatLayout/SwitchableChannelNavigation.tsx` passes `EndContent={SidebarToggleWhileChannelOpen}` to `ChannelListHeader`; `examples/vite/src/App.tsx` registers `ThreadListHeaderWithSidebarToggle` (`examples/vite/src/Sidebar/HeaderContent.tsx`) as `ThreadListHeader`
- Docs impact:
- `03-ui/03-components/01-channel-list-ui.md`, `03-ui/03-components/08-thread-list-ui.md`, `03-ui/03-components/12-overriding-components.md`, `06-component-reference/02-channel/04-componentcontext.md`, `06-component-reference/07-thread-list/01-threadlist.md`, `05-ui-cookbooks/10-layout/04-collapsible-sidebar.md`, `05-ui-cookbooks/03-search/03-mentions-inbox.md`, `08-release-guides/01-upgrade-to-v15.md`
- Example needed: yes (the guide section has one)

---

## Messages and message lists
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { PropsWithChildren } from 'react';
import {
ChannelHeader,
ChannelListHeader,
ChannelNavigation,
MessageComposer,
MessageList,
ThreadHeader,
useWorkspaceNavigation,
WorkspaceNavigationBackButton,
} from 'stream-chat-react';
import { ChannelSlot, ThreadSlot } from 'stream-chat-react/slot-layout';
Expand All @@ -18,8 +20,20 @@ export const layouts: LayoutDescriptor[] = [
{ id: 'channels', slots: ['channel', 'thread'] },
];

// The toggle, only while a channel is open beside the list. With nothing open, collapsing the
// sidebar would leave an empty screen.
const SidebarToggleWhileChannelOpen = () => {
const { openChannels } = useWorkspaceNavigation();
return openChannels.length > 0 ? <SidebarToggle /> : null;
};

// The default channel list header, with the toggle at its end.
const ChannelListHeaderWithToggle = () => (
<ChannelListHeader EndContent={SidebarToggleWhileChannelOpen} />
);

// Declared once, so WithComponents gets the same object on every render.
export const overrides = { HeaderEndContent: SidebarToggle };
export const overrides = { ChannelListHeader: ChannelListHeaderWithToggle };

// The toggle, then the back button the header shows by default.
const ChannelHeaderStart = () => (
Expand Down
4 changes: 2 additions & 2 deletions examples/vite/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ import {
} from './CustomMessageUi';
import { ConfigurableMessageActions } from './CustomMessageActions';
import { InlineEditableMessage } from './InlineEditMessage';
import { SidebarToggle } from './Sidebar/SidebarToggle.tsx';
import { ThreadListHeaderWithSidebarToggle } from './Sidebar/HeaderContent.tsx';
import { CommandModeAttachmentSelector } from './CommandModeAttachmentSelector.tsx';
import {
ConfigurableComposerPreviews,
Expand Down Expand Up @@ -694,7 +694,7 @@ const App = () => {
MessageReactions: CustomMessageReactions,
reactionOptions: newReactionOptions,
Search: CustomChannelSearch,
HeaderEndContent: SidebarToggle,
ThreadListHeader: ThreadListHeaderWithSidebarToggle,
MessageActions: ConfigurableMessageActions,
AttachmentSelector: CommandModeAttachmentSelector,
MessageBubbleContent: ConfigurableMessageBubbleContent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
useStateStore,
} from 'stream-chat-react';

import { SidebarToggleWhileChannelOpen } from '../Sidebar/HeaderContent.tsx';
import { ScrollToActiveChannelButton } from './ScrollToActiveChannelButton';

// Human-readable labels for each channel list, keyed by the paginator id set up in App.tsx.
Expand Down Expand Up @@ -221,7 +222,7 @@ export const SwitchableChannelNavigation = () => {

return (
<div className='str-chat__channel-list'>
<ChannelListHeader />
<ChannelListHeader EndContent={SidebarToggleWhileChannelOpen} />
<Search />
{!isActive && activePaginator && (
// Expose the active paginator through ChannelListContext (as the SDK's ChannelLists
Expand Down
26 changes: 25 additions & 1 deletion examples/vite/src/Sidebar/HeaderContent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { WorkspaceNavigationBackButton } from 'stream-chat-react';
import {
ThreadListHeader,
useWorkspaceNavigation,
WorkspaceNavigationBackButton,
} from 'stream-chat-react';

import { SidebarToggle } from './SidebarToggle.tsx';

Expand All @@ -10,3 +14,23 @@ export const HeaderStartWithSidebarToggle = () => (
<SidebarToggle />
</>
);

/** The sidebar toggle while a channel is open beside the channel list. With nothing open,
* collapsing the sidebar would leave an empty screen. */
export const SidebarToggleWhileChannelOpen = () => {
const { openChannels } = useWorkspaceNavigation();
return openChannels.length > 0 ? <SidebarToggle /> : null;
};

/** The sidebar toggle while a thread is open beside the thread list. With nothing open,
* collapsing the sidebar would leave an empty screen. */
export const SidebarToggleWhileThreadOpen = () => {
const { openThreads } = useWorkspaceNavigation();
return openThreads.length > 0 ? <SidebarToggle /> : null;
};

/** The thread list header, registered as the `ThreadListHeader` override, with the sidebar toggle
* at its end. */
export const ThreadListHeaderWithSidebarToggle = () => (
<ThreadListHeader EndContent={SidebarToggleWhileThreadOpen} />
);
25 changes: 15 additions & 10 deletions src/components/ChannelList/ChannelListHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import React from 'react';
import {
useComponentContext,
useTranslationContext,
useWorkspaceNavigation,
} from '../../context';
import { useTranslationContext } from '../../context';

export const ChannelListHeader = () => {
export type ChannelListHeaderProps = {
/** Rendered at the end of the header, after the title. No default. */
EndContent?: React.ComponentType;
/** Rendered at the start of the header, before the title. No default. */
StartContent?: React.ComponentType;
};

/** Header of the channel list: the "Chats" title, with optional start and end content. */
export const ChannelListHeader = ({
EndContent,
StartContent,
}: ChannelListHeaderProps) => {
const { t } = useTranslationContext();
const { HeaderEndContent } = useComponentContext();
// `HeaderEndContent` renders only while a channel is open in the workspace.
const hasActiveChannel = useWorkspaceNavigation().openChannels.length > 0;
return (
<div className='str-chat__channel-list__header'>
{StartContent && <StartContent />}
<div className='str-chat__channel-list__header__title'>
{t('channelList.header.chats.text', 'Chats')}
</div>
{hasActiveChannel && HeaderEndContent && <HeaderEndContent />}
{EndContent && <EndContent />}
</div>
);
};
Loading
Loading