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 = () => ( - + );