diff --git a/ai-docs/ai-migration-v14-v15.md b/ai-docs/ai-migration-v14-v15.md
index f7f3508ce..b5d8a7e4c 100644
--- a/ai-docs/ai-migration-v14-v15.md
+++ b/ai-docs/ai-migration-v14-v15.md
@@ -1690,7 +1690,12 @@ Where you pass the props depends on who renders the header:
Both headers are exported.
-To keep the v14 behavior of showing the content only next to an open channel or thread (a sidebar toggle, for example), make the content return `null` otherwise: read `openChannels` / `openThreads` from `useWorkspaceNavigation()`. `ChatView` (or your own `WorkspaceNavigationProvider`, [section 4](#4-selecting-channels-and-opening-threads-workspace-navigation)) fills these lists; without one they stay empty, so gate on your own selection state instead.
+To keep showing the content only while something is open beside the list (a sidebar toggle, for example), make it return `null` otherwise. Read the workspace from `useWorkspaceNavigation()`:
+
+- `openChannels` / `openThreads` list the channels and threads that the workspace's panels were opened with. Something opened over another item in the same panel (stacked) is not in them. In a workspace whose panels open with channels and threads only, a stacked item always sits over one that is in the lists, so "something is open" is either list being non-empty; `openThreads` alone misses a thread stacked over a channel.
+- A panel opened with another kind, such as a `userProfile` panel, is in neither list, even with a channel stacked over it. If your layout opens such panels, check the layout instead: `useLayoutViewState()` from `stream-chat-react/slot-layout` returns each slot's binding (`slotBindings`) and stacked layers (`slotLayers`).
+- To ask whether a given channel or thread is on screen, use `isChannelActive(cid)` / `isThreadActive(threadId)`, which follow what each panel shows.
+- `ChatView` (or your own `WorkspaceNavigationProvider`, [section 4](#4-selecting-channels-and-opening-threads-workspace-navigation)) fills all of these. Without one the lists stay empty, so gate on your own selection state instead.
```tsx
// v14
@@ -1708,24 +1713,18 @@ import {
// `SidebarToggle` stands for a control of your own.
const SidebarToggle = () => ;
-// Shown only while a channel is open, as in v14.
-const SidebarToggleWhileChannelOpen = () => {
- const { openChannels } = useWorkspaceNavigation();
- return openChannels.length > 0 ? : null;
-};
-
-// Shown only while a thread is open, as in v14.
-const SidebarToggleWhileThreadOpen = () => {
- const { openThreads } = useWorkspaceNavigation();
- return openThreads.length > 0 ? : null;
+// Shown only while a channel or thread is open beside the list.
+const SidebarToggleWhileContentOpen = () => {
+ const { openChannels, openThreads } = useWorkspaceNavigation();
+ return openChannels.length > 0 || openThreads.length > 0 ? : null;
};
const ChannelListHeaderWithToggle = () => (
-
+
);
const ThreadListHeaderWithToggle = () => (
-
+
);
// Declared once, so WithComponents gets the same object on every render.
diff --git a/ai-docs/v15-breaking-changes.md b/ai-docs/v15-breaking-changes.md
index 5ffe36ef9..b2203893a 100644
--- a/ai-docs/v15-breaking-changes.md
+++ b/ai-docs/v15-breaking-changes.md
@@ -2315,12 +2315,12 @@ const CustomThreadHeader = () => {
- `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.
- - 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.
+ - to show the content only while something is open beside the list, read `useWorkspaceNavigation()` in the content component and return `null` while both `openChannels` and `openThreads` are empty. The lists hold the channels and threads the workspace's panels were opened with: an item stacked over another one in a panel is not in them. Where panels open with channels and threads only, a stacked item always sits over one that is, so checking one list alone misses it (a thread stacked over a channel is not in `openThreads`). A panel opened with another kind (a `userProfile` panel, even with a channel stacked over it) is in neither list; such layouts check `useLayoutViewState()` (`slotBindings`, `slotLayers`) from `stream-chat-react/slot-layout` instead. Whether a given channel or thread is on screen is `isChannelActive(cid)` / `isThreadActive(threadId)`. `ChatView` or an app's own `WorkspaceNavigationProvider` fills these; without one the lists stay empty, so an app without either gates on its own selection state.
- Evidence:
- `release-v14:src/components/ChannelList/ChannelListHeader.tsx:15` `{channel && HeaderEndContent && }`; `release-v14:src/components/Threads/ThreadList/ThreadListHeader.tsx:12` `{activeThread && 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`
+ - examples: `examples/vite/src/ChatLayout/SwitchableChannelNavigation.tsx` passes `EndContent={SidebarToggleWhileContentOpen}` 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)
diff --git a/examples/cookbook/src/recipes/layout/collapsible-sidebar/views.tsx b/examples/cookbook/src/recipes/layout/collapsible-sidebar/views.tsx
index 0f32984ab..6d17ef4b0 100644
--- a/examples/cookbook/src/recipes/layout/collapsible-sidebar/views.tsx
+++ b/examples/cookbook/src/recipes/layout/collapsible-sidebar/views.tsx
@@ -20,16 +20,18 @@ 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 ? : null;
+// The toggle, only while a channel or thread is open beside the list. With nothing open,
+// collapsing the sidebar would leave an empty screen. `openChannels` / `openThreads` hold the
+// channels and threads the panels were opened with, so checking both covers a thread stacked over
+// a channel.
+const SidebarToggleWhileContentOpen = () => {
+ const { openChannels, openThreads } = useWorkspaceNavigation();
+ return openChannels.length > 0 || openThreads.length > 0 ? : null;
};
// The default channel list header, with the toggle at its end.
const ChannelListHeaderWithToggle = () => (
-
+
);
// Declared once, so WithComponents gets the same object on every render.
diff --git a/examples/vite/src/ChatLayout/SwitchableChannelNavigation.tsx b/examples/vite/src/ChatLayout/SwitchableChannelNavigation.tsx
index 53a50fc94..2592c3ff9 100644
--- a/examples/vite/src/ChatLayout/SwitchableChannelNavigation.tsx
+++ b/examples/vite/src/ChatLayout/SwitchableChannelNavigation.tsx
@@ -25,7 +25,7 @@ import {
useStateStore,
} from 'stream-chat-react';
-import { SidebarToggleWhileChannelOpen } from '../Sidebar/HeaderContent.tsx';
+import { SidebarToggleWhileContentOpen } 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.
@@ -222,7 +222,7 @@ export const SwitchableChannelNavigation = () => {
return (
-
+
{!isActive && activePaginator && (
// Expose the active paginator through ChannelListContext (as the SDK's ChannelLists
diff --git a/examples/vite/src/Sidebar/HeaderContent.tsx b/examples/vite/src/Sidebar/HeaderContent.tsx
index 30e977db9..764fbc455 100644
--- a/examples/vite/src/Sidebar/HeaderContent.tsx
+++ b/examples/vite/src/Sidebar/HeaderContent.tsx
@@ -15,22 +15,18 @@ export const HeaderStartWithSidebarToggle = () => (
>
);
-/** 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 ? : 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 ? : null;
+/** The sidebar toggle while a channel or thread is open beside the list. With nothing open,
+ * collapsing the sidebar would leave an empty screen. `openChannels` / `openThreads` hold the
+ * channels and threads the panels were opened with, so checking both covers a thread stacked over
+ * a channel. A panel opened with a user profile is in neither list, and the toggle stays hidden
+ * next to it. */
+export const SidebarToggleWhileContentOpen = () => {
+ const { openChannels, openThreads } = useWorkspaceNavigation();
+ return openChannels.length > 0 || openThreads.length > 0 ? : null;
};
/** The thread list header, registered as the `ThreadListHeader` override, with the sidebar toggle
* at its end. */
export const ThreadListHeaderWithSidebarToggle = () => (
-
+
);