{displayTitle}
diff --git a/src/components/ChannelHeader/__tests__/ChannelHeader.test.tsx b/src/components/ChannelHeader/__tests__/ChannelHeader.test.tsx
index 5f9dd6d0ed..d75ed975f1 100644
--- a/src/components/ChannelHeader/__tests__/ChannelHeader.test.tsx
+++ b/src/components/ChannelHeader/__tests__/ChannelHeader.test.tsx
@@ -6,6 +6,7 @@ import { ChannelHeader } from '../ChannelHeader';
import { ChannelStateProvider } from '../../../context/ChannelStateContext';
import { ChatProvider } from '../../../context/ChatContext';
+import { WithComponents } from '../../../context/WithComponents';
import { TranslationProvider } from '../../../context/TranslationContext';
import {
dispatchUserUpdatedEvent,
@@ -40,7 +41,6 @@ const user2 = generateUser({ image: null });
let testChannel1: ChannelAPIResponse;
let client: StreamChat;
-const CustomMenuIcon = () =>
Custom Menu Icon
;
const defaultChannelState = {
members: [generateMember({ user: user1 }), generateMember({ user: user2 })],
};
@@ -180,21 +180,35 @@ describe('ChannelHeader', () => {
});
});
- it('should render the sidebar toggle button when sidebar is collapsed', async () => {
- const { container } = await renderComponent();
- // The ToggleSidebarButton renders when navOpen is falsy (not provided in mock context)
- // or when on mobile viewport. In jsdom it sees !navOpen so the button shows.
- const toggleButton = container.querySelector('.str-chat__header-sidebar-toggle');
- expect(toggleButton).toBeInTheDocument();
- });
+ describe('HeaderStartContent slot', () => {
+ const HeaderStartContent = () =>
;
- it('should display custom menu icon', async () => {
- const { container } = await renderComponent({
- props: {
- MenuIcon: CustomMenuIcon,
- },
+ it('should not render HeaderStartContent when not provided via ComponentContext', async () => {
+ await renderComponent();
+ expect(screen.queryByTestId('sidebar-toggle')).not.toBeInTheDocument();
+ });
+
+ it('should render HeaderStartContent when provided via ComponentContext', async () => {
+ client = await getTestClientWithUser(user1);
+ testChannel1 = generateChannel({ ...defaultChannelState });
+ useMockedApis(client, [getOrCreateChannelApi(testChannel1)]);
+ const channel = client.channel('messaging', testChannel1.channel.id);
+ await channel.query();
+
+ render(
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ expect(screen.getByTestId('sidebar-toggle')).toBeInTheDocument();
});
- expect(container.querySelector('div#custom-icon')).toBeInTheDocument();
});
it("DM channel should reflect change of other user's name", async () => {
diff --git a/src/components/ChannelHeader/hooks/useIsMobileViewport.ts b/src/components/ChannelHeader/hooks/useIsMobileViewport.ts
deleted file mode 100644
index 1ad58ce3c8..0000000000
--- a/src/components/ChannelHeader/hooks/useIsMobileViewport.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { useEffect, useState } from 'react';
-
-import { NAV_SIDEBAR_DESKTOP_BREAKPOINT } from '../../Chat/hooks/useChat';
-
-const mobileQuery = () =>
- typeof window !== 'undefined'
- ? window.matchMedia(`(max-width: ${NAV_SIDEBAR_DESKTOP_BREAKPOINT - 1}px)`)
- : null;
-
-/** True when viewport width is below NAV_SIDEBAR_DESKTOP_BREAKPOINT (768px). */
-export const useIsMobileViewport = (): boolean => {
- const [isMobile, setIsMobile] = useState(() => mobileQuery()?.matches ?? false);
-
- useEffect(() => {
- const mql = mobileQuery();
- if (!mql) return;
- const handler = () => setIsMobile(mql.matches);
- handler();
- mql.addEventListener('change', handler);
- return () => mql.removeEventListener('change', handler);
- }, []);
-
- return isMobile;
-};
diff --git a/src/components/ChannelHeader/plan.md b/src/components/ChannelHeader/plan.md
index a06fe46dac..5cb704c54d 100644
--- a/src/components/ChannelHeader/plan.md
+++ b/src/components/ChannelHeader/plan.md
@@ -30,9 +30,9 @@ Tasks are self-contained; styling and component tasks have a dependency order. A
**Key design elements (channel header section):**
-- Layout: `[hamburger or sidebar icon] | [channelName + Online stacked] | [avatar right]`
-- Variant: Sidebar collapsed (shows `IconLayoutAlignLeft`) vs expanded (hamburger / MenuIcon)
-- Avatar on the right (current impl has it between hamburger and title)
+- Layout: `[sidebar toggle slot] | [channelName + Online stacked] | [avatar right]`
+- Sidebar toggle is provided externally via `HeaderStartContent` in `ComponentContext`
+- Avatar on the right (current impl has it between toggle and title)
---
@@ -63,33 +63,11 @@ Tasks are self-contained; styling and component tasks have a dependency order. A
---
-## Task 2: Add Sidebar-Collapsed Variant Styles
-
-**File(s) to create/modify:** `src/components/ChannelHeader/styling/ChannelHeader.scss`
-
-**Dependencies:** Task 1
-
-**Status:** pending
-
-**Owner:** unassigned
-
-**Scope:**
-
-- Add modifier: `.str-chat__channel-header--sidebar-collapsed` (compact left icon area if needed)
-- Use existing `--str-chat__*` vars from design-system-tokens
-- Hamburger (MenuIcon) and sidebar toggle (`IconLayoutAlignLeft`) button styling
-
-**Acceptance Criteria:**
-
-- [ ] Sidebar-collapsed modifier applies correctly when class is present
-
----
-
-## Task 3: Register ChannelHeader Styles and Update Component
+## Task 2: Register ChannelHeader Styles and Update Component
**File(s) to create/modify:** `src/styling/index.scss`, `src/components/ChannelHeader/ChannelHeader.tsx`
-**Dependencies:** Task 1, Task 2
+**Dependencies:** Task 1
**Status:** pending
@@ -99,27 +77,26 @@ Tasks are self-contained; styling and component tasks have a dependency order. A
- Add `@use '../components/ChannelHeader/styling' as ChannelHeader` to `src/styling/index.scss` in the appropriate group (alphabetical, chat components)
- Update `ChannelHeader.tsx`:
- - Reorder layout: hamburger/sidebar icon | text block (title + Online) | avatar (right)
- - Add optional prop: `sidebarCollapsed?: boolean`
- - Render `MenuIcon` (hamburger) when expanded, `IconLayoutAlignLeft` when `sidebarCollapsed` — import from `src/components/Icons/icons.tsx`
+ - Reorder layout: sidebar toggle slot | text block (title + Online) | avatar (right)
+ - The sidebar toggle is provided externally via the `HeaderStartContent` slot in `ComponentContext` (no built-in toggle or `MenuIcon` prop)
- Simplify info line to "Online" (or keep watcher_count: "X online") per design
- - Apply modifier class: `str-chat__channel-header--sidebar-collapsed` when `sidebarCollapsed`
-- Preserve: `live`, `subtitle`, `member_count`, `Avatar`, `MenuIcon`, `title`, `image` — ensure backward compatibility
+- Preserve: `live`, `subtitle`, `member_count`, `Avatar`, `title`, `image` — ensure backward compatibility
+- Note: sidebar collapsed/expanded state is NOT managed by the SDK; the app owns sidebar visibility
**Acceptance Criteria:**
- [ ] ChannelHeader styles imported in `src/styling/index.scss`
- [ ] Component layout matches Figma: avatar on right, text in middle
-- [ ] New prop `sidebarCollapsed` works and applies modifier
+- [ ] `HeaderStartContent` slot renders when provided via `ComponentContext`
- [ ] Existing tests pass; update tests if needed for new structure
---
-## Task 4: Integration and Tests
+## Task 3: Integration and Tests
**File(s) to create/modify:** `src/components/ChannelHeader/__tests__/ChannelHeader.test.js`
-**Dependencies:** Task 3
+**Dependencies:** Task 2
**Status:** pending
@@ -128,7 +105,7 @@ Tasks are self-contained; styling and component tasks have a dependency order. A
**Scope:**
- Run existing tests; fix any failures from layout/class changes
-- Add tests for `sidebarCollapsed` when applicable
+- Add tests for `HeaderStartContent` slot rendering when applicable
- Ensure `yarn test`, `yarn types`, `yarn lint-fix` pass
**Acceptance Criteria:**
@@ -141,12 +118,11 @@ Tasks are self-contained; styling and component tasks have a dependency order. A
## Execution Order
-| Phase | Tasks | Can run in parallel? |
-| ----- | ------ | ------------------------- |
-| 1 | Task 1 | — |
-| 2 | Task 2 | No (depends on Task 1) |
-| 3 | Task 3 | No (depends on Task 1, 2) |
-| 4 | Task 4 | No (depends on Task 3) |
+| Phase | Tasks | Can run in parallel? |
+| ----- | ------ | ---------------------- |
+| 1 | Task 1 | — |
+| 2 | Task 2 | No (depends on Task 1) |
+| 3 | Task 3 | No (depends on Task 2) |
---
@@ -155,9 +131,8 @@ Tasks are self-contained; styling and component tasks have a dependency order. A
| Task | Creates | Modifies |
| ---- | ------------------------------------------------------------------------------ | ----------------------------------------------------------- |
| 1 | `ChannelHeader/styling/ChannelHeader.scss`, `ChannelHeader/styling/index.scss` | — |
-| 2 | — | `ChannelHeader/styling/ChannelHeader.scss` |
-| 3 | — | `src/styling/index.scss`, `ChannelHeader/ChannelHeader.tsx` |
-| 4 | — | `ChannelHeader/__tests__/ChannelHeader.test.js` |
+| 2 | — | `src/styling/index.scss`, `ChannelHeader/ChannelHeader.tsx` |
+| 3 | — | `ChannelHeader/__tests__/ChannelHeader.test.js` |
---
@@ -165,5 +140,5 @@ Tasks are self-contained; styling and component tasks have a dependency order. A
- ChannelHeader currently has no dedicated styling folder; styles may come from stream-chat-css. This plan introduces ChannelHeader/styling per dev-patterns.
- Loading channel header in `Channel/styling/Channel.scss` uses `--str-chat__channel-header-background-color`; keep variable usage consistent.
-- Backward compatibility: `sidebarCollapsed` defaults to `false`; existing usage unchanged.
-- Sidebar expansion toggle icon: use `IconLayoutAlignLeft` from `src/components/Icons/icons.tsx` when `sidebarCollapsed=true`.
+- Sidebar toggle is externally provided via `HeaderStartContent` slot in `ComponentContext`. The SDK does not own sidebar state — apps provide their own toggle via `WithComponents`.
+- No `sidebarCollapsed` prop or `MenuIcon` prop — these were removed as part of the navOpen removal.
diff --git a/src/components/ChannelHeader/styling/ChannelHeader.scss b/src/components/ChannelHeader/styling/ChannelHeader.scss
index 11b7d4d0a3..ff93ce6335 100644
--- a/src/components/ChannelHeader/styling/ChannelHeader.scss
+++ b/src/components/ChannelHeader/styling/ChannelHeader.scss
@@ -63,10 +63,4 @@
font: var(--str-chat__caption-default-text);
color: var(--str-chat__channel-header__data__subtitle-color);
}
-
- &.str-chat__channel-header--sidebar-collapsed {
- .str-chat__header-sidebar-toggle {
- // Compact styling when sidebar collapsed
- }
- }
}
diff --git a/src/components/ChannelList/ChannelList.tsx b/src/components/ChannelList/ChannelList.tsx
index 95679a3a51..aef6418d35 100644
--- a/src/components/ChannelList/ChannelList.tsx
+++ b/src/components/ChannelList/ChannelList.tsx
@@ -11,7 +11,6 @@ import type {
} from 'stream-chat';
import { useConnectionRecoveredListener } from './hooks/useConnectionRecoveredListener';
-import { useMobileNavigation } from './hooks/useMobileNavigation';
import type { CustomQueryChannelsFn } from './hooks/usePaginatedChannels';
import { usePaginatedChannels } from './hooks/usePaginatedChannels';
import {
@@ -185,9 +184,7 @@ const UnMemoizedChannelList = (props: ChannelListProps) => {
channel,
channelsQueryState,
client,
- closeMobileNav,
customClasses,
- navOpen = true,
searchController,
setActiveChannel,
theme,
@@ -270,8 +267,6 @@ const UnMemoizedChannelList = (props: ChannelListProps) => {
? channelRenderFilterFn(channels)
: channels;
- useMobileNavigation(channelListRef, navOpen, closeMobileNav);
-
const { customHandler, defaultHandler } = usePrepareShapeHandlers({
allowNewMessagesFromUnfilteredChannels,
filters,
@@ -335,7 +330,6 @@ const UnMemoizedChannelList = (props: ChannelListProps) => {
{
'str-chat--windows-flags':
useImageFlagEmojisOnWindows && navigator.userAgent.match(/Win/),
- [`${baseClass}--open`]: navOpen,
},
);
diff --git a/src/components/ChannelList/ChannelListHeader.tsx b/src/components/ChannelList/ChannelListHeader.tsx
index 9d80af4e47..ca47e2444d 100644
--- a/src/components/ChannelList/ChannelListHeader.tsx
+++ b/src/components/ChannelList/ChannelListHeader.tsx
@@ -1,28 +1,18 @@
-import React, { type ComponentType } from 'react';
-import clsx from 'clsx';
-import { useChatContext, useTranslationContext } from '../../context';
-import { IconSidebar } from '../Icons';
-import { ToggleSidebarButton } from '../Button/ToggleSidebarButton';
+import React from 'react';
+import {
+ useChatContext,
+ useComponentContext,
+ useTranslationContext,
+} from '../../context';
-export type ChannelListHeaderProps = {
- ToggleButtonIcon?: ComponentType;
-};
-
-export const ChannelListHeader = ({
- ToggleButtonIcon = IconSidebar,
-}: ChannelListHeaderProps) => {
+export const ChannelListHeader = () => {
const { t } = useTranslationContext();
- const { channel, navOpen } = useChatContext();
+ const { channel } = useChatContext();
+ const { HeaderEndContent } = useComponentContext();
return (
-
+
{t('Chats')}
-
-
-
+ {channel && HeaderEndContent &&
}
);
};
diff --git a/src/components/ChannelList/__tests__/ChannelList.test.tsx b/src/components/ChannelList/__tests__/ChannelList.test.tsx
index 62075da26f..e22d5d7cf4 100644
--- a/src/components/ChannelList/__tests__/ChannelList.test.tsx
+++ b/src/components/ChannelList/__tests__/ChannelList.test.tsx
@@ -113,96 +113,6 @@ describe('ChannelList', () => {
vi.restoreAllMocks();
});
- describe('mobile navigation', () => {
- let closeMobileNav: Mock;
- let props: ChannelListProps & { closeMobileNav: Mock };
- beforeEach(() => {
- closeMobileNav = vi.fn();
- props = {
- closeMobileNav,
- filters: {},
- };
- useMockedApis(chatClient, [queryChannelsApi([])]);
- });
- it('should call `closeMobileNav` prop function, when clicked outside ChannelList', async () => {
- Object.defineProperty(window, 'innerWidth', { value: 500, writable: true });
- const { container, getByRole, getByTestId } = await render(
-
({
- channelsQueryState: channelsQueryStateMock,
- client: chatClient,
- closeMobileNav,
- navOpen: true,
- searchController: new SearchController(),
- })}
- >
-
-
-
-
- ,
- );
- // Wait for list of channels to load in DOM.
- await waitFor(() => {
- expect(getByRole('list')).toBeInTheDocument();
- });
-
- await act(() => {
- fireEvent.click(getByTestId('outside-channellist'));
- });
-
- await waitFor(() => {
- expect(closeMobileNav).toHaveBeenCalledTimes(1);
- });
- const results = await axe(container);
- expect(results).toHaveNoViolations();
- Object.defineProperty(window, 'innerWidth', { value: 1024, writable: true });
- });
-
- it('should not call `closeMobileNav` prop function on click, if ChannelList is collapsed', async () => {
- const { container, getByRole, getByTestId } = await render(
-
({
- channelsQueryState: channelsQueryStateMock,
- client: chatClient,
- closeMobileNav,
- navOpen: false,
- searchController: new SearchController(),
- })}
- >
-
-
-
-
- ,
- );
-
- // Wait for list of channels to load in DOM.
- await waitFor(() => {
- expect(getByRole('list')).toBeInTheDocument();
- });
-
- await act(() => {
- fireEvent.click(getByTestId('outside-channellist'));
- });
- await waitFor(() => {
- expect(closeMobileNav).toHaveBeenCalledTimes(0);
- });
- const results = await axe(container);
- expect(results).toHaveNoViolations();
- });
- });
-
it('should re-query channels when filters change', async () => {
const props = {
filters: {},
diff --git a/src/components/ChannelList/__tests__/ChannelListHeader.test.tsx b/src/components/ChannelList/__tests__/ChannelListHeader.test.tsx
new file mode 100644
index 0000000000..a0c9f9ab26
--- /dev/null
+++ b/src/components/ChannelList/__tests__/ChannelListHeader.test.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import { cleanup, render, screen } from '@testing-library/react';
+import { ChatProvider, WithComponents } from '../../../context';
+import { TranslationProvider } from '../../../context/TranslationContext';
+import { mockChatContext, mockTranslationContextValue } from '../../../mock-builders';
+import { ChannelListHeader } from '../ChannelListHeader';
+
+const t = vi.fn((key: string) => key);
+const HeaderEndContent = () =>
;
+
+afterEach(cleanup);
+
+describe('ChannelListHeader', () => {
+ it('should not render HeaderEndContent when not provided via ComponentContext', () => {
+ render(
+
+
+
+
+ ,
+ );
+
+ expect(screen.queryByTestId('sidebar-toggle')).not.toBeInTheDocument();
+ });
+
+ it('should render HeaderEndContent when a channel is active', () => {
+ render(
+
+
+
+
+
+
+ ,
+ );
+
+ expect(screen.getByTestId('sidebar-toggle')).toBeInTheDocument();
+ });
+
+ it('should not render HeaderEndContent when no channel is active', () => {
+ render(
+
+
+
+
+
+
+ ,
+ );
+
+ expect(screen.queryByTestId('sidebar-toggle')).not.toBeInTheDocument();
+ });
+});
diff --git a/src/components/ChannelList/hooks/index.ts b/src/components/ChannelList/hooks/index.ts
index 85c9bf0e04..421def9a09 100644
--- a/src/components/ChannelList/hooks/index.ts
+++ b/src/components/ChannelList/hooks/index.ts
@@ -1,5 +1,4 @@
export * from './useConnectionRecoveredListener';
-export * from './useMobileNavigation';
export * from './usePaginatedChannels';
export * from './useChannelMembershipState';
export * from './useChannelMembersState';
diff --git a/src/components/ChannelList/hooks/useMobileNavigation.ts b/src/components/ChannelList/hooks/useMobileNavigation.ts
deleted file mode 100644
index d938c84d5b..0000000000
--- a/src/components/ChannelList/hooks/useMobileNavigation.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { useEffect } from 'react';
-
-const MOBILE_NAV_BREAKPOINT = 768;
-
-export const useMobileNavigation = (
- channelListRef: React.RefObject
,
- navOpen: boolean,
- closeMobileNav?: () => void,
-) => {
- useEffect(() => {
- const isClickInsideChannelList = (event: MouseEvent) => {
- const channelListElement = channelListRef.current;
-
- if (!channelListElement) return false;
-
- const eventPath = event.composedPath();
-
- // `event.target` may become detached before this document listener runs.
- // Use composedPath to reliably detect whether the click originated inside.
- return eventPath.includes(channelListElement);
- };
-
- const handleClickOutside = (event: MouseEvent) => {
- if (typeof window !== 'undefined' && window.innerWidth >= MOBILE_NAV_BREAKPOINT) {
- return;
- }
- if (
- closeMobileNav &&
- channelListRef.current &&
- !isClickInsideChannelList(event) &&
- navOpen
- ) {
- closeMobileNav();
- }
- };
-
- document.addEventListener('click', handleClickOutside);
-
- return () => {
- document.removeEventListener('click', handleClickOutside);
- };
- }, [channelListRef, closeMobileNav, navOpen]);
-};
diff --git a/src/components/ChannelList/styling/ChannelList.scss b/src/components/ChannelList/styling/ChannelList.scss
index daa566489a..0d74f6782f 100644
--- a/src/components/ChannelList/styling/ChannelList.scss
+++ b/src/components/ChannelList/styling/ChannelList.scss
@@ -194,37 +194,6 @@
inset-inline-start: var(--str-chat__chat-view-selector-mobile-width, 0px);
width: calc(100% - var(--str-chat__chat-view-selector-mobile-width, 0px));
}
-
- &.str-chat__channel-list--open {
- pointer-events: auto;
- transform: translateX(0);
- transition-delay: 0s, 0s;
- visibility: visible;
- }
- }
-
- /* Desktop (≥768px): collapse when nav closed so main content uses space. */
- @media (min-width: 768px) {
- &.str-chat__channel-list--open {
- flex-basis: var(--str-chat__channel-list-width);
- max-width: 100%;
- min-width: 280px;
- opacity: 1;
- pointer-events: auto;
- transform: translateX(0);
- width: var(--str-chat__channel-list-width);
- }
-
- &:not(.str-chat__channel-list--open) {
- flex: 0 0 0;
- width: 0;
- min-width: 0;
- max-width: 0;
- opacity: 0;
- overflow: hidden;
- pointer-events: none;
- transform: translateX(calc(0px - var(--str-chat__channel-list-transition-offset)));
- }
}
@media (prefers-reduced-motion: reduce) {
diff --git a/src/components/ChannelList/styling/ChannelListHeader.scss b/src/components/ChannelList/styling/ChannelListHeader.scss
index 734d1d9ac2..b95684a1e3 100644
--- a/src/components/ChannelList/styling/ChannelListHeader.scss
+++ b/src/components/ChannelList/styling/ChannelListHeader.scss
@@ -1,14 +1,7 @@
.str-chat__channel-list__header {
display: flex;
align-items: center;
- opacity: 1;
padding: var(--spacing-md);
- transform: translateX(0);
- transition:
- opacity var(--str-chat__channel-list-transition-duration, 180ms)
- var(--str-chat__channel-list-transition-easing, ease),
- transform var(--str-chat__channel-list-transition-duration, 180ms)
- var(--str-chat__channel-list-transition-easing, ease);
width: 100%;
.str-chat__channel-list__header__title {
@@ -16,25 +9,4 @@
font: var(--str-chat__heading-lg-text);
color: var(--text-primary);
}
-
- &.str-chat__channel-list__header--sidebar-collapsed {
- opacity: 0;
- pointer-events: none;
- transform: translateX(
- calc(0px - var(--str-chat__channel-list-transition-offset, 8px))
- );
- }
-
- @media (max-width: 767px) {
- transition: none;
-
- &.str-chat__channel-list__header--sidebar-collapsed {
- opacity: 1;
- transform: none;
- }
- }
-
- @media (prefers-reduced-motion: reduce) {
- transition: none;
- }
}
diff --git a/src/components/Chat/Chat.tsx b/src/components/Chat/Chat.tsx
index d56123937a..53c2fca8e4 100644
--- a/src/components/Chat/Chat.tsx
+++ b/src/components/Chat/Chat.tsx
@@ -27,10 +27,6 @@ export type ChatProps = {
defaultLanguage?: SupportedTranslations;
/** Instance of Stream i18n */
i18nInstance?: Streami18n;
- /**
- * Initial open state of the sidebar. Omit for responsive (viewport-derived); set to true/false for an explicit initial state.
- */
- initialNavOpen?: boolean;
/** Instance of SearchController class that allows to control all the search operations. */
searchController?: SearchController;
/** Used for injecting className/s to the Channel and ChannelList components */
@@ -56,7 +52,6 @@ export const Chat = (props: PropsWithChildren) => {
customClasses,
defaultLanguage,
i18nInstance,
- initialNavOpen,
isMessageAIGenerated,
searchController: customChannelSearchController,
theme = 'messaging light',
@@ -65,19 +60,15 @@ export const Chat = (props: PropsWithChildren) => {
const {
channel,
- closeMobileNav,
getAppSettings,
latestMessageDatesByChannels,
mutes,
- navOpen,
- openMobileNav,
setActiveChannel,
translators,
} = useChat({
client,
defaultLanguage,
i18nInstance,
- initialNavOpen,
});
const channelsQueryState = useChannelsQueryState();
@@ -99,14 +90,11 @@ export const Chat = (props: PropsWithChildren) => {
channel,
channelsQueryState,
client,
- closeMobileNav,
customClasses,
getAppSettings,
isMessageAIGenerated,
latestMessageDatesByChannels,
mutes,
- navOpen,
- openMobileNav,
searchController,
setActiveChannel,
theme,
diff --git a/src/components/Chat/__tests__/Chat.test.tsx b/src/components/Chat/__tests__/Chat.test.tsx
index 40da2a9f90..9ef07b863d 100644
--- a/src/components/Chat/__tests__/Chat.test.tsx
+++ b/src/components/Chat/__tests__/Chat.test.tsx
@@ -61,11 +61,8 @@ describe('Chat', () => {
expect(context.client).toBe(chatClient);
expect(context.channel).toBeUndefined();
expect(context.mutes).toStrictEqual([]);
- expect(context.navOpen).toBe(true);
expect(context.theme).toBe('messaging light');
expect(context.setActiveChannel).toBeInstanceOf(Function);
- expect(context.openMobileNav).toBeInstanceOf(Function);
- expect(context.closeMobileNav).toBeInstanceOf(Function);
expect(context.client.getUserAgent()).toMatch(
new RegExp(
`^stream-chat-react-.+-${originalUserAgent.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`,
@@ -108,102 +105,6 @@ describe('Chat', () => {
});
});
- describe('mobile nav', () => {
- it('initialNavOpen prop should set navOpen', async () => {
- let context: ChatContextValue;
- await act(() => {
- render(
-
- {
- context = ctx;
- }}
- />
- ,
- );
- });
-
- await waitFor(() => expect(context.navOpen).toBe(false));
- });
-
- it('initialNavOpen prop update should be ignored', async () => {
- let context: ChatContextValue;
- const { rerender } = render(
-
- {
- context = ctx;
- }}
- />
- ,
- );
- await waitFor(() => expect(context.navOpen).toBe(false));
-
- rerender(
-
- {
- context = ctx;
- }}
- />
- ,
- );
- await waitFor(() => expect(context.navOpen).toBe(false));
- });
-
- it('open/close fn updates the nav state', async () => {
- let context: ChatContextValue;
- render(
-
- {
- context = ctx;
- }}
- />
- ,
- );
-
- await waitFor(() => expect(context.navOpen).toBe(true));
- act(() => context.closeMobileNav());
- await waitFor(() => expect(context.navOpen).toBe(false));
- act(() => {
- context.openMobileNav();
- });
-
- await waitFor(() => expect(context.navOpen).toBe(true));
- });
-
- it('setActiveChannel closes the nav', async () => {
- const originalInnerWidth = window.innerWidth;
- Object.defineProperty(window, 'innerWidth', {
- configurable: true,
- value: 500,
- writable: true,
- });
-
- let context: ChatContextValue;
- render(
-
- {
- context = ctx;
- }}
- />
- ,
- );
-
- await waitFor(() => expect(context.navOpen).toBe(true));
- await act(() => context.setActiveChannel());
- await waitFor(() => expect(context.navOpen).toBe(false));
-
- Object.defineProperty(window, 'innerWidth', {
- configurable: true,
- value: originalInnerWidth,
- writable: true,
- });
- });
- });
-
describe('mutes', () => {
it('init the mute state with client data', async () => {
const chatClientWithUser = await getTestClientWithUser({ id: 'user_x' });
diff --git a/src/components/Chat/hooks/useChat.ts b/src/components/Chat/hooks/useChat.ts
index 1d8085055b..0af9603cfd 100644
--- a/src/components/Chat/hooks/useChat.ts
+++ b/src/components/Chat/hooks/useChat.ts
@@ -18,27 +18,16 @@ import type {
StreamChat,
} from 'stream-chat';
-/** Viewport width (px) above which the sidebar is open by default when using responsive initial nav state. */
-export const NAV_SIDEBAR_DESKTOP_BREAKPOINT = 768;
-
-/** With responsive nav: sidebar is open on load (so ChannelList/ThreadList + selector visible); close on channel/thread selection. */
-const getDefaultNavOpenFromViewport = (): boolean => true;
-
export type UseChatParams = {
client: StreamChat;
defaultLanguage?: SupportedTranslations;
i18nInstance?: Streami18n;
- /**
- * Initial open state of the sidebar. Omit for responsive (viewport-derived); set to true/false for an explicit initial state.
- */
- initialNavOpen?: boolean;
};
export const useChat = ({
client,
defaultLanguage = 'en',
i18nInstance,
- initialNavOpen,
}: UseChatParams) => {
const [translators, setTranslators] = useState({
t: defaultTranslatorFunction,
@@ -48,17 +37,10 @@ export const useChat = ({
const [channel, setChannel] = useState();
const [mutes, setMutes] = useState>([]);
- const [navOpen, setNavOpen] = useState(() => {
- if (initialNavOpen === undefined) return getDefaultNavOpenFromViewport() ?? true;
- return initialNavOpen;
- });
const [latestMessageDatesByChannels, setLatestMessageDatesByChannels] = useState({});
const clientMutes = (client.user as OwnUserResponse)?.mutes ?? [];
- const closeMobileNav = () => setNavOpen(false);
- const openMobileNav = () => setTimeout(() => setNavOpen(true), 100);
-
const appSettings = useRef | null>(null);
const getAppSettings = () => {
@@ -144,10 +126,6 @@ export const useChat = ({
}
setChannel(activeChannel);
- const isMobileViewport =
- typeof window !== 'undefined' &&
- window.innerWidth < NAV_SIDEBAR_DESKTOP_BREAKPOINT;
- if (isMobileViewport) closeMobileNav();
},
[],
);
@@ -158,12 +136,9 @@ export const useChat = ({
return {
channel,
- closeMobileNav,
getAppSettings,
latestMessageDatesByChannels,
mutes,
- navOpen,
- openMobileNav,
setActiveChannel,
translators,
};
diff --git a/src/components/Chat/hooks/useCreateChatContext.ts b/src/components/Chat/hooks/useCreateChatContext.ts
index 5cf96e11fb..6337a415de 100644
--- a/src/components/Chat/hooks/useCreateChatContext.ts
+++ b/src/components/Chat/hooks/useCreateChatContext.ts
@@ -7,14 +7,11 @@ export const useCreateChatContext = (value: ChatContextValue) => {
channel,
channelsQueryState,
client,
- closeMobileNav,
customClasses,
getAppSettings,
isMessageAIGenerated,
latestMessageDatesByChannels,
mutes,
- navOpen,
- openMobileNav,
searchController,
setActiveChannel,
theme,
@@ -35,14 +32,11 @@ export const useCreateChatContext = (value: ChatContextValue) => {
channel,
channelsQueryState,
client,
- closeMobileNav,
customClasses,
getAppSettings,
isMessageAIGenerated,
latestMessageDatesByChannels,
mutes,
- navOpen,
- openMobileNav,
searchController,
setActiveChannel,
theme,
@@ -57,7 +51,6 @@ export const useCreateChatContext = (value: ChatContextValue) => {
getAppSettings,
searchController,
mutedUsersLength,
- navOpen,
isMessageAIGenerated,
],
);
diff --git a/src/components/ChatView/ChatView.tsx b/src/components/ChatView/ChatView.tsx
index 89f06671fc..89ae49ab8a 100644
--- a/src/components/ChatView/ChatView.tsx
+++ b/src/components/ChatView/ChatView.tsx
@@ -237,7 +237,6 @@ export const ChatViewChannelsSelectorButton = ({
iconOnly = true,
}: ChatViewSelectorItemProps) => {
const { activeChatView, setActiveChatView } = useChatViewContext();
- const { openMobileNav } = useChatContext('ChatViewChannelsSelectorButton');
const { t } = useTranslationContext();
const isActive = activeChatView === 'channels';
@@ -248,10 +247,7 @@ export const ChatViewChannelsSelectorButton = ({
Icon={IconMessageBubble}
iconOnly={iconOnly}
isActive={isActive}
- onPointerDown={() => {
- openMobileNav();
- setActiveChatView('channels');
- }}
+ onPointerDown={() => setActiveChatView('channels')}
text={t('Channels')}
/>
);
@@ -260,7 +256,7 @@ export const ChatViewChannelsSelectorButton = ({
export const ChatViewThreadsSelectorButton = ({
iconOnly = true,
}: ChatViewSelectorItemProps) => {
- const { client, openMobileNav } = useChatContext();
+ const { client } = useChatContext();
const { unreadThreadCount } = useStateStore(
client.threads.state,
unreadThreadCountSelector,
@@ -278,10 +274,7 @@ export const ChatViewThreadsSelectorButton = ({
Icon={IconThread}
iconOnly={iconOnly}
isActive={isActive}
- onPointerDown={() => {
- openMobileNav();
- setActiveChatView('threads');
- }}
+ onPointerDown={() => setActiveChatView('threads')}
text={t('Threads')}
>
@@ -317,21 +310,13 @@ export const defaultChatViewSelectorItemSet: ChatViewSelectorEntry[] = [
const ChatViewSelector = ({
iconOnly = true,
itemSet = defaultChatViewSelectorItemSet,
-}: ChatViewSelectorProps) => {
- const { navOpen } = useChatContext('ChatView.Selector');
- return (
-
- {itemSet.map(({ Component, type }) => (
-
- ))}
-
- );
-};
+}: ChatViewSelectorProps) => (
+
+ {itemSet.map(({ Component, type }) => (
+
+ ))}
+
+);
ChatView.Channels = ChannelsView;
ChatView.Threads = ThreadsView;
diff --git a/src/components/ChatView/__tests__/ChatView.test.tsx b/src/components/ChatView/__tests__/ChatView.test.tsx
index e07e9826de..e5b9aec40c 100644
--- a/src/components/ChatView/__tests__/ChatView.test.tsx
+++ b/src/components/ChatView/__tests__/ChatView.test.tsx
@@ -48,11 +48,9 @@ const renderComponent = async (threadManagerState: any) => {
value={{
channelsQueryState: fromPartial({}),
client,
- closeMobileNav: vi.fn(),
getAppSettings: vi.fn(),
latestMessageDatesByChannels: {},
mutes: [],
- openMobileNav: vi.fn(),
searchController: fromPartial({}),
setActiveChannel: vi.fn(),
theme: 'messaging light',
@@ -82,11 +80,9 @@ const renderSelector = async (selectorProps?: any) => {
value={{
channelsQueryState: fromPartial({}),
client,
- closeMobileNav: vi.fn(),
getAppSettings: vi.fn(),
latestMessageDatesByChannels: {},
mutes: [],
- openMobileNav: vi.fn(),
searchController: fromPartial({}),
setActiveChannel: vi.fn(),
theme: 'messaging light',
diff --git a/src/components/ChatView/styling/ChatView.scss b/src/components/ChatView/styling/ChatView.scss
index 3a3fc8978e..fc28c7a0a5 100644
--- a/src/components/ChatView/styling/ChatView.scss
+++ b/src/components/ChatView/styling/ChatView.scss
@@ -29,47 +29,6 @@
border-inline-end: 1px solid var(--str-chat-selector-border-color);
background-color: var(--str-chat-selector-background-color);
- /* Mobile: hide when nav closed, show when nav open. */
- @media (max-width: 767px) {
- &.str-chat__chat-view__selector--nav-closed {
- pointer-events: none;
- transform: translateX(
- calc(0px - var(--str-chat__chat-view-selector-transition-offset))
- );
- visibility: hidden;
- }
-
- inset-block: 0;
- inset-inline-start: 0;
- position: absolute;
- transition:
- transform var(--str-chat__chat-view-selector-transition-duration)
- var(--str-chat__chat-view-selector-transition-easing),
- visibility 0s linear var(--str-chat__chat-view-selector-transition-duration);
- width: var(--str-chat__chat-view-selector-mobile-width);
- z-index: 2;
-
- &.str-chat__chat-view__selector--nav-open {
- pointer-events: auto;
- transform: translateX(0);
- transition-delay: 0s, 0s;
- visibility: visible;
- }
- }
-
- /* Desktop (≥768px): collapse when nav closed so main content uses space. */
- @media (min-width: 768px) {
- &.str-chat__chat-view__selector--nav-closed {
- width: 0;
- min-width: 0;
- overflow: hidden;
- padding-inline: 0;
- padding-block: 0;
- gap: 0;
- border-inline-end: none;
- }
- }
-
.str-chat__chat-view__selector-button-container {
display: flex;
position: relative;
diff --git a/src/components/Icons/icons.tsx b/src/components/Icons/icons.tsx
index ad22f76f9c..bb6664edca 100644
--- a/src/components/Icons/icons.tsx
+++ b/src/components/Icons/icons.tsx
@@ -577,20 +577,6 @@ export const IconImage = createIcon(
/>,
);
-// was: IconLayoutAlignLeft
-export const IconSidebar = createIcon(
- 'IconSidebar',
- ,
- { 'data-rtl-mirror': '' },
-);
-
// was: IconMagnifyingGlassSearch
export const IconSearch = createIcon(
'IconSearch',
diff --git a/src/components/Thread/ThreadHeader.tsx b/src/components/Thread/ThreadHeader.tsx
index ed82a8803e..aaef556287 100644
--- a/src/components/Thread/ThreadHeader.tsx
+++ b/src/components/Thread/ThreadHeader.tsx
@@ -7,13 +7,13 @@ import { useChannelPreviewInfo } from '../ChannelListItem/hooks/useChannelPrevie
import { TypingIndicatorHeader } from '../TypingIndicator/TypingIndicatorHeader';
import { useThreadContext } from '../Threads';
import { useChatContext } from '../../context/ChatContext';
+import { useComponentContext } from '../../context/ComponentContext';
import { useTypingContext } from '../../context/TypingContext';
import type { LocalMessage } from 'stream-chat';
import type { ThreadState } from 'stream-chat';
import { Button } from '../Button';
-import { IconSidebar, IconXmark } from '../Icons';
-import { ToggleSidebarButton } from '../Button/ToggleSidebarButton';
+import { IconXmark } from '../Icons';
import { useChatViewContext } from '../ChatView';
const threadStateSelector = ({ replyCount }: ThreadState) => ({ replyCount });
@@ -67,17 +67,16 @@ export type ThreadHeaderProps = {
closeThread: (event?: React.BaseSyntheticEvent) => void;
/** The thread parent message */
thread: LocalMessage;
- /** UI component to display menu icon, defaults to IconSidebar*/
- MenuIcon?: React.ComponentType;
/** Override the thread display title */
overrideTitle?: string;
};
export const ThreadHeader = (props: ThreadHeaderProps) => {
- const { closeThread, MenuIcon = IconSidebar, overrideTitle, thread } = props;
+ const { closeThread, overrideTitle, thread } = props;
const { t } = useTranslationContext();
const { channel } = useChannelStateContext();
+ const { HeaderStartContent } = useComponentContext();
const { activeChatView } = useChatViewContext();
const { displayTitle: channelDisplayTitle } = useChannelPreviewInfo({ channel });
@@ -101,11 +100,7 @@ export const ThreadHeader = (props: ThreadHeaderProps) => {
return (
- {activeChatView === 'threads' && (
-
-
-
- )}
+ {activeChatView === 'threads' && HeaderStartContent && }
{t('Thread')}
diff --git a/src/components/Thread/__tests__/ThreadHeader.test.tsx b/src/components/Thread/__tests__/ThreadHeader.test.tsx
index 52ac00f23a..6880d99a9b 100644
--- a/src/components/Thread/__tests__/ThreadHeader.test.tsx
+++ b/src/components/Thread/__tests__/ThreadHeader.test.tsx
@@ -27,12 +27,6 @@ vi.mock('../../TypingIndicator/TypingIndicatorHeader', () => ({
TypingIndicatorHeader: () =>
Typing...
,
}));
-vi.mock('../../Button/ToggleSidebarButton', () => ({
- ToggleSidebarButton: ({ children }) => (
-
{children}
- ),
-}));
-
vi.mock('../../Threads', () => ({
useThreadContext: vi.fn(() => undefined),
}));
@@ -93,10 +87,7 @@ const renderComponent = ({
({
client,
- closeMobileNav: vi.fn(),
latestMessageDatesByChannels: {},
- navOpen: false,
- openMobileNav: vi.fn(),
})}
>
@@ -181,30 +172,4 @@ describe('ThreadHeader', () => {
expect(screen.getByText('2 replies')).toBeInTheDocument();
expect(screen.queryByText(/^undefined ·/)).not.toBeInTheDocument();
});
-
- it('does not render the sidebar toggle in the channels view', () => {
- vi.mocked(useChannelPreviewInfo).mockReturnValue(
- fromPartial({ displayTitle: 'Bob' }),
- );
-
- renderComponent({
- activeChatView: 'channels',
- threadContext: { id: 'thread-1' },
- });
-
- expect(screen.queryByTestId('toggle-sidebar-button')).not.toBeInTheDocument();
- });
-
- it('renders the sidebar toggle in the threads view', () => {
- vi.mocked(useChannelPreviewInfo).mockReturnValue(
- fromPartial({ displayTitle: 'Bob' }),
- );
-
- renderComponent({
- activeChatView: 'threads',
- threadContext: { id: 'thread-1' },
- });
-
- expect(screen.getByTestId('toggle-sidebar-button')).toBeInTheDocument();
- });
});
diff --git a/src/components/Threads/ThreadList/ThreadList.tsx b/src/components/Threads/ThreadList/ThreadList.tsx
index d41547cdd2..49fbef9c9b 100644
--- a/src/components/Threads/ThreadList/ThreadList.tsx
+++ b/src/components/Threads/ThreadList/ThreadList.tsx
@@ -1,8 +1,6 @@
import React, { useEffect, useState } from 'react';
import type { ComputeItemKey, VirtuosoProps } from 'react-virtuoso';
import { Virtuoso } from 'react-virtuoso';
-import clsx from 'clsx';
-
import type { Thread, ThreadManager, ThreadManagerState } from 'stream-chat';
import { ThreadListItem as DefaultThreadListItem } from './ThreadListItem';
@@ -85,7 +83,7 @@ const useThreadHighlighting = (threadManager: ThreadManager) => {
};
export const ThreadList = ({ virtuosoProps }: ThreadListProps) => {
- const { client, navOpen = true } = useChatContext();
+ const { client } = useChatContext();
const {
NotificationList: NotificationListFromContext = NotificationList,
ThreadListEmptyPlaceholder = DefaultThreadListEmptyPlaceholder,
@@ -101,11 +99,7 @@ export const ThreadList = ({ virtuosoProps }: ThreadListProps) => {
if (isLoading && !threads.length) {
return (
-
+
@@ -115,11 +109,7 @@ export const ThreadList = ({ virtuosoProps }: ThreadListProps) => {
}
return (
-
+
{/* TODO: allow re-load on stale ThreadManager state */}
diff --git a/src/components/Threads/ThreadList/ThreadListHeader.tsx b/src/components/Threads/ThreadList/ThreadListHeader.tsx
index 14f3eb7587..5b3047e8b2 100644
--- a/src/components/Threads/ThreadList/ThreadListHeader.tsx
+++ b/src/components/Threads/ThreadList/ThreadListHeader.tsx
@@ -1,30 +1,15 @@
-import React, { type ComponentType } from 'react';
-import clsx from 'clsx';
-import { useChatContext, useTranslationContext } from '../../../context';
-import { IconSidebar } from '../../Icons';
+import React from 'react';
+import { useComponentContext, useTranslationContext } from '../../../context';
import { useThreadsViewContext } from '../../ChatView';
-import { ToggleSidebarButton } from '../../Button/ToggleSidebarButton';
-export type ChannelListHeaderProps = {
- ToggleButtonIcon?: ComponentType;
-};
-
-export const ThreadListHeader = ({
- ToggleButtonIcon = IconSidebar,
-}: ChannelListHeaderProps) => {
+export const ThreadListHeader = () => {
const { t } = useTranslationContext();
- const { navOpen } = useChatContext();
+ const { HeaderEndContent } = useComponentContext();
const { activeThread } = useThreadsViewContext();
return (
-
+
{t('Threads')}
-
-
-
+ {activeThread && HeaderEndContent &&
}
);
};
diff --git a/src/components/Threads/ThreadList/ThreadListItemUI.tsx b/src/components/Threads/ThreadList/ThreadListItemUI.tsx
index 2340aa625a..519c73dc29 100644
--- a/src/components/Threads/ThreadList/ThreadListItemUI.tsx
+++ b/src/components/Threads/ThreadList/ThreadListItemUI.tsx
@@ -13,7 +13,6 @@ import { useThreadListItemContext } from './ThreadListItem';
import { useStateStore } from '../../../store';
import { Badge } from '../../Badge';
import { SummarizedMessagePreview } from '../../SummarizedMessagePreview';
-import { NAV_SIDEBAR_DESKTOP_BREAKPOINT } from '../../Chat';
export type ThreadListItemUIProps = ComponentPropsWithoutRef<'button'> & {
resetHighlighting?: () => void;
@@ -54,7 +53,6 @@ export const ThreadListItemUI = ({
const { displayTitle: channelDisplayTitle } = useChannelPreviewInfo({ channel });
const { t } = useTranslationContext('ThreadListItemUI');
- const { closeMobileNav } = useChatContext('ThreadListItemUI');
const { activeThread, setActiveThread } = useThreadsViewContext();
const avatarProps: Partial
| undefined = deletedAt
@@ -96,15 +94,7 @@ export const ThreadListItemUI = ({
typeof resetHighlighting !== 'undefined',
})}
data-thread-id={thread.id}
- onClick={() => {
- if (
- typeof window !== 'undefined' &&
- window.innerWidth < NAV_SIDEBAR_DESKTOP_BREAKPOINT
- ) {
- closeMobileNav();
- }
- setActiveThread(thread);
- }}
+ onClick={() => setActiveThread(thread)}
role='option'
{...props}
>
diff --git a/src/components/Threads/ThreadList/__tests__/ThreadList.test.tsx b/src/components/Threads/ThreadList/__tests__/ThreadList.test.tsx
index 2c637793df..5309f7472a 100644
--- a/src/components/Threads/ThreadList/__tests__/ThreadList.test.tsx
+++ b/src/components/Threads/ThreadList/__tests__/ThreadList.test.tsx
@@ -60,7 +60,7 @@ describe('ThreadList', () => {
};
beforeEach(() => {
- mockUseChatContext.mockReturnValue({ client: mockClient, navOpen: true });
+ mockUseChatContext.mockReturnValue({ client: mockClient });
mockUseComponentContext.mockReturnValue({});
mockUseStateStore.mockReturnValue({ isLoading: false, threads: [] });
});
diff --git a/src/components/Threads/ThreadList/__tests__/ThreadListHeader.test.tsx b/src/components/Threads/ThreadList/__tests__/ThreadListHeader.test.tsx
new file mode 100644
index 0000000000..693d4f2399
--- /dev/null
+++ b/src/components/Threads/ThreadList/__tests__/ThreadListHeader.test.tsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import { cleanup, render, screen } from '@testing-library/react';
+import { fromPartial } from '@total-typescript/shoehorn';
+import type { Thread } from 'stream-chat';
+import { ChatProvider, WithComponents } from '../../../../context';
+import { TranslationProvider } from '../../../../context/TranslationContext';
+import { mockChatContext, mockTranslationContextValue } from '../../../../mock-builders';
+import { ThreadListHeader } from '../ThreadListHeader';
+
+vi.mock('../../../ChatView', () => ({
+ useThreadsViewContext: vi.fn(() => ({ activeThread: undefined })),
+}));
+
+import { useThreadsViewContext } from '../../../ChatView';
+
+const t = vi.fn((key: string) => key);
+const HeaderEndContent = () => ;
+
+afterEach(cleanup);
+
+describe('ThreadListHeader', () => {
+ it('should not render HeaderEndContent when not provided via ComponentContext', () => {
+ render(
+
+
+
+
+ ,
+ );
+
+ expect(screen.queryByTestId('sidebar-toggle')).not.toBeInTheDocument();
+ });
+
+ it('should render HeaderEndContent when a thread is active', () => {
+ vi.mocked(useThreadsViewContext).mockReturnValue({
+ activeThread: fromPartial({}),
+ setActiveThread: vi.fn(),
+ });
+
+ render(
+
+
+
+
+
+
+ ,
+ );
+
+ expect(screen.getByTestId('sidebar-toggle')).toBeInTheDocument();
+ });
+
+ it('should not render HeaderEndContent when no thread is active', () => {
+ vi.mocked(useThreadsViewContext).mockReturnValue({
+ activeThread: undefined,
+ setActiveThread: vi.fn(),
+ });
+
+ render(
+
+
+
+
+
+
+ ,
+ );
+
+ expect(screen.queryByTestId('sidebar-toggle')).not.toBeInTheDocument();
+ });
+});
diff --git a/src/components/Threads/ThreadList/styling/ThreadList.scss b/src/components/Threads/ThreadList/styling/ThreadList.scss
index 4335bcd4c3..35e77141f3 100644
--- a/src/components/Threads/ThreadList/styling/ThreadList.scss
+++ b/src/components/Threads/ThreadList/styling/ThreadList.scss
@@ -70,43 +70,6 @@
visibility: hidden;
width: var(--str-chat__thread-list-mobile-width);
z-index: 1;
-
- &.str-chat__thread-list-container--open {
- box-shadow: var(--str-chat__thread-list-box-shadow);
- pointer-events: auto;
- transform: translateX(0);
- transition-delay: 0s, 0s;
- visibility: visible;
-
- .str-chat__chat-view & {
- inset-inline-start: var(--str-chat__chat-view-selector-mobile-width, 0px);
- width: calc(100% - var(--str-chat__chat-view-selector-mobile-width, 0px));
- }
- }
- }
-
- /* Desktop (≥768px): collapse when nav closed so main content uses space. */
- @media (min-width: 768px) {
- &.str-chat__thread-list-container--open {
- flex-basis: var(--str-chat__thread-list-width);
- max-width: 100%;
- min-width: var(--str-chat__thread-list-min-width);
- opacity: 1;
- pointer-events: auto;
- transform: translateX(0);
- width: var(--str-chat__thread-list-width);
- }
-
- &:not(.str-chat__thread-list-container--open) {
- flex: 0 0 0;
- width: 0;
- min-width: 0;
- max-width: 0;
- opacity: 0;
- overflow: hidden;
- pointer-events: none;
- transform: translateX(calc(0px - var(--str-chat__thread-list-transition-offset)));
- }
}
@media (prefers-reduced-motion: reduce) {
diff --git a/src/components/Threads/ThreadList/styling/ThreadListHeader.scss b/src/components/Threads/ThreadList/styling/ThreadListHeader.scss
index a60e2cd744..7ea4de58cb 100644
--- a/src/components/Threads/ThreadList/styling/ThreadListHeader.scss
+++ b/src/components/Threads/ThreadList/styling/ThreadListHeader.scss
@@ -1,15 +1,8 @@
.str-chat__thread-list__header {
display: flex;
align-items: center;
- opacity: 1;
padding: var(--spacing-md);
height: var(--str-chat__channel-header-height);
- transform: translateX(0);
- transition:
- opacity var(--str-chat__channel-list-transition-duration, 180ms)
- var(--str-chat__channel-list-transition-easing, ease),
- transform var(--str-chat__channel-list-transition-duration, 180ms)
- var(--str-chat__channel-list-transition-easing, ease);
width: 100%;
.str-chat__thread-list__header__title {
@@ -17,29 +10,4 @@
font: var(--str-chat__heading-lg-text);
color: var(--text-primary);
}
-
- &.str-chat__thread-list__header--sidebar-collapsed {
- opacity: 0;
- pointer-events: none;
- transform: translateX(
- calc(0px - var(--str-chat__channel-list-transition-offset, 8px))
- );
-
- .str-chat__header-sidebar-toggle {
- // Compact styling when sidebar collapsed
- }
- }
-
- @media (max-width: 767px) {
- transition: none;
-
- &.str-chat__thread-list__header--sidebar-collapsed {
- opacity: 1;
- transform: none;
- }
- }
-
- @media (prefers-reduced-motion: reduce) {
- transition: none;
- }
}
diff --git a/src/context/ChatContext.tsx b/src/context/ChatContext.tsx
index fbd7f74c7a..83af30a3de 100644
--- a/src/context/ChatContext.tsx
+++ b/src/context/ChatContext.tsx
@@ -31,11 +31,9 @@ export type ChatContextValue = {
* Indicates, whether a channels query has been triggered within ChannelList by its channels pagination controller.
*/
channelsQueryState: ChannelsQueryState;
- closeMobileNav: () => void;
getAppSettings: () => Promise | null;
latestMessageDatesByChannels: Record;
mutes: Array;
- openMobileNav: () => void;
/** Instance of SearchController class that allows to control all the search operations. */
searchController: SearchController;
/**
@@ -58,7 +56,6 @@ export type ChatContextValue = {
* Object through which custom classes can be set for main container components of the SDK.
*/
customClasses?: CustomClasses;
- navOpen?: boolean;
} & Partial> &
Required>;
diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx
index d2466c012e..b61a3cfe54 100644
--- a/src/context/ComponentContext.tsx
+++ b/src/context/ComponentContext.tsx
@@ -261,6 +261,10 @@ export type ComponentContextValue = {
TypingIndicator?: React.ComponentType;
/** Custom UI component that indicates a user is viewing unread messages. It disappears once the user scrolls to UnreadMessagesSeparator. Defaults to and accepts same props as: [UnreadMessagesNotification](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageList/UnreadMessagesNotification.tsx) */
UnreadMessagesNotification?: React.ComponentType;
+ /** Custom UI component rendered at the end of sidebar headers (ChannelListHeader, ThreadListHeader). No default — if omitted, the slot is empty. */
+ HeaderEndContent?: React.ComponentType;
+ /** Custom UI component rendered at the start of content headers (ChannelHeader, ThreadHeader). No default — if omitted, the slot is empty. */
+ HeaderStartContent?: React.ComponentType;
/** Custom UI component that separates read messages from unread, defaults to and accepts same props as: [UnreadMessagesSeparator](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageList/UnreadMessagesSeparator.tsx) */
UnreadMessagesSeparator?: React.ComponentType;
/** Component used to play video. If not provided, ReactPlayer is used as a default video player. */