Skip to content
Open
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
35 changes: 35 additions & 0 deletions examples/vite/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import {
import {
Attachment,
type AttachmentProps,
Button,
Chat,
ChatView,
createIcon,
MessageReactions,
type NotificationListProps,
NotificationList,
Expand All @@ -26,7 +28,9 @@ import {
defaultReactionOptions,
type ReactionOptions,
mapEmojiMartData,
useChatContext,
useCreateChatClient,
useTranslationContext,
Search,
} from 'stream-chat-react';
import { createTextComposerEmojiMiddleware, EmojiPicker } from 'stream-chat-react/emojis';
Expand Down Expand Up @@ -162,6 +166,36 @@ const ConfigurableNotificationList = (props: NotificationListProps) => {
return <NotificationList {...props} verticalAlignment={verticalAlignment} />;
};

const IconSidebar = createIcon(
'IconSidebar',
<path
d='M6.875 3.75V16.25M3.125 3.75H16.875C17.2202 3.75 17.5 4.02982 17.5 4.375V15.625C17.5 15.9702 17.2202 16.25 16.875 16.25H3.125C2.77982 16.25 2.5 15.9702 2.5 15.625V4.375C2.5 4.02982 2.77982 3.75 3.125 3.75Z'
fill='none'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='1.5'
/>,
);

const SidebarToggle = () => {
const { closeMobileNav, navOpen, openMobileNav } = useChatContext();
const { t } = useTranslationContext();
return (
<Button
appearance='ghost'
aria-label={navOpen ? t('aria/Collapse sidebar') : t('aria/Expand sidebar')}
circular
className='str-chat__header-sidebar-toggle'
onClick={navOpen ? closeMobileNav : openMobileNav}
size='md'
variant='secondary'
>
<IconSidebar />
</Button>
);
};

const language = new URLSearchParams(window.location.search).get('language');
const i18nInstance = language ? new Streami18n({ language: language as any }) : undefined;

Expand Down Expand Up @@ -358,6 +392,7 @@ const App = () => {
MessageReactions: CustomMessageReactions,
reactionOptions: newReactionOptions,
Search: CustomChannelSearch,
SidebarToggle,
...messageUiOverrides,
}}
>
Expand Down
36 changes: 0 additions & 36 deletions src/components/Button/ToggleSidebarButton.tsx

This file was deleted.

17 changes: 4 additions & 13 deletions src/components/ChannelHeader/ChannelHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import React from 'react';

import { IconSidebar } from '../Icons/icons';
import { type ChannelAvatarProps, ChannelAvatar as DefaultAvatar } from '../Avatar';
import { TypingIndicatorHeader } from '../TypingIndicator/TypingIndicatorHeader';
import { useChannelHeaderOnlineStatus } from './hooks/useChannelHeaderOnlineStatus';
import { useChannelPreviewInfo } from '../ChannelListItem/hooks/useChannelPreviewInfo';
import { useChannelStateContext } from '../../context/ChannelStateContext';
import { useChatContext } from '../../context/ChatContext';
import { useComponentContext } from '../../context/ComponentContext';
import { useTypingContext } from '../../context/TypingContext';
import clsx from 'clsx';
import { ToggleSidebarButton } from '../Button/ToggleSidebarButton';

const ChannelHeaderSubtitle = () => {
const { channelConfig } = useChannelStateContext('ChannelHeaderSubtitle');
Expand Down Expand Up @@ -40,8 +39,6 @@ export type ChannelHeaderProps = {
Avatar?: React.ComponentType<ChannelAvatarProps>;
/** Manually set the image to render, defaults to the Channel image */
image?: string;
/** UI component to display menu icon, defaults to IconSidebar*/
MenuIcon?: React.ComponentType;
/** Set title manually */
title?: string;
};
Expand All @@ -50,15 +47,11 @@ export type ChannelHeaderProps = {
* The ChannelHeader component renders some basic information about a Channel.
*/
export const ChannelHeader = (props: ChannelHeaderProps) => {
const {
Avatar = DefaultAvatar,
image: overrideImage,
MenuIcon = IconSidebar,
title: overrideTitle,
} = props;
const { Avatar = DefaultAvatar, image: overrideImage, title: overrideTitle } = props;

const { channel } = useChannelStateContext();
const { navOpen } = useChatContext();
const { SidebarToggle } = useComponentContext();
const { displayImage, displayTitle, groupChannelDisplayInfo } = useChannelPreviewInfo({
channel,
overrideImage,
Expand All @@ -72,9 +65,7 @@ export const ChannelHeader = (props: ChannelHeaderProps) => {
})}
>
<div className='str-chat__channel-header__start'>
<ToggleSidebarButton mode='expand'>
<MenuIcon />
</ToggleSidebarButton>
{!navOpen && SidebarToggle && <SidebarToggle />}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oliverlaz navOpen state is used in many places to coordinate the channel list / thread list display, so not sure we can remove it. On the other hand we need to make sure we document how sidebar display should be handled in the docs probably - some cookbook. WDYT?

</div>
<div className='str-chat__channel-header__data'>
<div className='str-chat__channel-header__data__title'>{displayTitle}</div>
Expand Down
64 changes: 50 additions & 14 deletions src/components/ChannelHeader/__tests__/ChannelHeader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -40,7 +41,6 @@ const user2 = generateUser({ image: null });
let testChannel1: ChannelAPIResponse;
let client: StreamChat;

const CustomMenuIcon = () => <div id='custom-icon'>Custom Menu Icon</div>;
const defaultChannelState = {
members: [generateMember({ user: user1 }), generateMember({ user: user2 })],
};
Expand Down Expand Up @@ -180,21 +180,57 @@ 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('SidebarToggle slot', () => {
const SidebarToggle = () => <div data-testid='sidebar-toggle' />;

it('should display custom menu icon', async () => {
const { container } = await renderComponent({
props: {
MenuIcon: CustomMenuIcon,
},
it('should not render SidebarToggle when not provided via ComponentContext', async () => {
await renderComponent();
expect(screen.queryByTestId('sidebar-toggle')).not.toBeInTheDocument();
});

it('should render SidebarToggle when navOpen is false', async () => {
client = await getTestClientWithUser(user1);
testChannel1 = generateChannel({ ...defaultChannelState });
useMockedApis(client, [getOrCreateChannelApi(testChannel1)]);
const channel = client.channel('messaging', testChannel1.channel.id);
await channel.query();

render(
<WithComponents overrides={{ SidebarToggle }}>
<ChatProvider value={mockChatContext({ channel, client, navOpen: false })}>
<ChannelStateProvider value={mockChannelStateContext({ channel })}>
<TranslationProvider value={mockTranslationContextValue({ t })}>
<ChannelHeader />
</TranslationProvider>
</ChannelStateProvider>
</ChatProvider>
</WithComponents>,
);

expect(screen.getByTestId('sidebar-toggle')).toBeInTheDocument();
});

it('should not render SidebarToggle when navOpen is true', async () => {
client = await getTestClientWithUser(user1);
testChannel1 = generateChannel({ ...defaultChannelState });
useMockedApis(client, [getOrCreateChannelApi(testChannel1)]);
const channel = client.channel('messaging', testChannel1.channel.id);
await channel.query();

render(
<WithComponents overrides={{ SidebarToggle }}>
<ChatProvider value={mockChatContext({ channel, client, navOpen: true })}>
<ChannelStateProvider value={mockChannelStateContext({ channel })}>
<TranslationProvider value={mockTranslationContextValue({ t })}>
<ChannelHeader />
</TranslationProvider>
</ChannelStateProvider>
</ChatProvider>
</WithComponents>,
);

expect(screen.queryByTestId('sidebar-toggle')).not.toBeInTheDocument();
});
expect(container.querySelector('div#custom-icon')).toBeInTheDocument();
});

it("DM channel should reflect change of other user's name", async () => {
Expand Down
23 changes: 9 additions & 14 deletions src/components/ChannelList/ChannelListHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
import React, { type ComponentType } from 'react';
import React from 'react';
import clsx from 'clsx';
import { useChatContext, useTranslationContext } from '../../context';
import { IconSidebar } from '../Icons';
import { ToggleSidebarButton } from '../Button/ToggleSidebarButton';
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 { SidebarToggle } = useComponentContext();
return (
<div
className={clsx('str-chat__channel-list__header', {
'str-chat__channel-list__header--sidebar-collapsed': !navOpen,
})}
>
<div className='str-chat__channel-list__header__title'>{t('Chats')}</div>
<ToggleSidebarButton canCollapse={!!channel} mode={'collapse'}>
<ToggleButtonIcon />
</ToggleSidebarButton>
{channel && SidebarToggle && <SidebarToggle />}
</div>
);
};
53 changes: 53 additions & 0 deletions src/components/ChannelList/__tests__/ChannelListHeader.test.tsx
Original file line number Diff line number Diff line change
@@ -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 SidebarToggle = () => <div data-testid='sidebar-toggle' />;

afterEach(cleanup);

describe('ChannelListHeader', () => {
it('should not render SidebarToggle when not provided via ComponentContext', () => {
render(
<ChatProvider value={mockChatContext()}>
<TranslationProvider value={mockTranslationContextValue({ t })}>
<ChannelListHeader />
</TranslationProvider>
</ChatProvider>,
);

expect(screen.queryByTestId('sidebar-toggle')).not.toBeInTheDocument();
});

it('should render SidebarToggle when a channel is active', () => {
render(
<WithComponents overrides={{ SidebarToggle }}>
<ChatProvider value={mockChatContext({ channel: {} })}>
<TranslationProvider value={mockTranslationContextValue({ t })}>
<ChannelListHeader />
</TranslationProvider>
</ChatProvider>
</WithComponents>,
);

expect(screen.getByTestId('sidebar-toggle')).toBeInTheDocument();
});

it('should not render SidebarToggle when no channel is active', () => {
render(
<WithComponents overrides={{ SidebarToggle }}>
<ChatProvider value={mockChatContext({ channel: undefined })}>
<TranslationProvider value={mockTranslationContextValue({ t })}>
<ChannelListHeader />
</TranslationProvider>
</ChatProvider>
</WithComponents>,
);

expect(screen.queryByTestId('sidebar-toggle')).not.toBeInTheDocument();
});
});
13 changes: 0 additions & 13 deletions src/components/Icons/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -573,19 +573,6 @@ export const IconImage = createIcon(
/>,
);

// was: IconLayoutAlignLeft
export const IconSidebar = createIcon(
'IconSidebar',
<path
d='M6.875 3.75V16.25M3.125 3.75H16.875C17.2202 3.75 17.5 4.02982 17.5 4.375V15.625C17.5 15.9702 17.2202 16.25 16.875 16.25H3.125C2.77982 16.25 2.5 15.9702 2.5 15.625V4.375C2.5 4.02982 2.77982 3.75 3.125 3.75Z'
fill='none'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='1.5'
/>,
);

// was: IconMagnifyingGlassSearch
export const IconSearch = createIcon(
'IconSearch',
Expand Down
Loading
Loading