From 679df5b933b535a04ec94a3659dc0781ebbfd1a7 Mon Sep 17 00:00:00 2001 From: martincupela Date: Tue, 17 Mar 2026 16:37:15 +0100 Subject: [PATCH] fix: show ToggleSidebarButton in threads view's ThreadHeader --- src/components/Thread/ThreadHeader.tsx | 22 +++- .../Thread/__tests__/ThreadHeader.test.js | 102 +++++++++++++++--- 2 files changed, 102 insertions(+), 22 deletions(-) diff --git a/src/components/Thread/ThreadHeader.tsx b/src/components/Thread/ThreadHeader.tsx index b82ee6d24f..cb953b38c4 100644 --- a/src/components/Thread/ThreadHeader.tsx +++ b/src/components/Thread/ThreadHeader.tsx @@ -12,7 +12,9 @@ import { useTypingContext } from '../../context/TypingContext'; import type { LocalMessage } from 'stream-chat'; import type { ThreadState } from 'stream-chat'; import { Button } from '../Button'; -import { IconCrossMedium } from '../Icons'; +import { IconCrossMedium, IconLayoutAlignLeft } from '../Icons'; +import { ToggleSidebarButton } from '../Button/ToggleSidebarButton'; +import { useChatViewContext } from '../ChatView'; const threadStateSelector = ({ replyCount }: ThreadState) => ({ replyCount }); @@ -40,8 +42,10 @@ const ThreadHeaderSubtitle = ({ ({ parent_id, user }) => user?.id !== client.user?.id && parent_id === parentId, ); const hasTyping = channelConfig?.typing_events !== false && typingInThread.length > 0; - const defaultSubtitle = - threadDisplayName + ' · ' + t('replyCount', { count: replyCount ?? 0 }); + const replyCountText = t('replyCount', { count: replyCount ?? 0 }); + const defaultSubtitle = threadDisplayName + ? `${threadDisplayName} · ${replyCountText}` + : replyCountText; return (
void; /** The thread parent message */ thread: LocalMessage; + /** UI component to display menu icon, defaults to IconLayoutAlignLeft*/ + MenuIcon?: React.ComponentType; /** Override the thread display title */ overrideTitle?: string; }; export const ThreadHeader = (props: ThreadHeaderProps) => { - const { closeThread, overrideTitle, thread } = props; + const { closeThread, MenuIcon = IconLayoutAlignLeft, overrideTitle, thread } = props; const { t } = useTranslationContext(); - const { channel } = useChannelStateContext('ThreadHeader'); + const { channel } = useChannelStateContext(); + const { activeChatView } = useChatViewContext(); const { displayTitle: channelDisplayTitle } = useChannelPreviewInfo({ channel }); const threadInstance = useThreadContext(); @@ -93,6 +100,11 @@ export const ThreadHeader = (props: ThreadHeaderProps) => { return (
+ {activeChatView === 'threads' && ( + + + + )}
{t('Thread')}
({ - useChannelDisplayName: jest.fn(), +jest.mock('../../ChannelPreview/hooks/useChannelPreviewInfo', () => ({ + useChannelPreviewInfo: jest.fn(() => ({ displayTitle: undefined })), +})); + +jest.mock('../../../store', () => ({ + useStateStore: jest.fn(() => undefined), +})); + +jest.mock('../../../context/TypingContext', () => ({ + useTypingContext: jest.fn(() => ({ typing: {} })), +})); + +jest.mock('../../TypingIndicator/TypingIndicatorHeader', () => ({ + TypingIndicatorHeader: () =>
Typing...
, +})); + +jest.mock('../../Button/ToggleSidebarButton', () => ({ + ToggleSidebarButton: ({ children }) => ( +
{children}
+ ), })); jest.mock('../../Threads', () => ({ useThreadContext: jest.fn(() => undefined), })); +jest.mock('../../ChatView', () => ({ + useChatViewContext: jest.fn(() => ({ activeChatView: 'channels' })), +})); + +const { + useChannelPreviewInfo, +} = require('../../ChannelPreview/hooks/useChannelPreviewInfo'); +const { useChatViewContext } = require('../../ChatView'); +const { useThreadContext } = require('../../Threads'); + const alice = { id: 'alice', name: 'Alice' }; const bob = { id: 'bob', name: 'Bob' }; + const createThread = (user) => ({ id: `${user?.id ?? 'thread'}-message`, reply_count: 2, @@ -36,20 +64,38 @@ const createChannel = (overrides = {}) => ({ ...overrides, }); -const renderComponent = ({ channelOverrides = {}, props = {} } = {}) => { - const client = { off: jest.fn(), on: jest.fn(), userID: alice.id }; +const renderComponent = ({ + activeChatView = 'channels', + channelOverrides = {}, + props = {}, + threadContext = undefined, +} = {}) => { + const client = { off: jest.fn(), on: jest.fn(), user: alice, userID: alice.id }; const thread = createThread(alice); const channel = createChannel(channelOverrides); + useChatViewContext.mockReturnValue({ + activeChatView, + setActiveChatView: jest.fn(), + }); + useThreadContext.mockReturnValue(threadContext); + return render( - - + + { if (key === 'Thread') return 'Thread'; if (key === 'replyCount') return `${options.count} replies`; - if (key === 'Direct message') return 'Direct message'; if (key === 'aria/Close thread') return 'Close thread'; return key; @@ -69,18 +115,18 @@ describe('ThreadHeader', () => { jest.clearAllMocks(); }); - it('renders the channel display title in the subtitle', async () => { - useChannelDisplayName.mockReturnValue('Bob'); + it('renders the channel display title in the subtitle', () => { + useChannelPreviewInfo.mockReturnValue({ displayTitle: 'Bob' }); - await renderComponent(); + renderComponent(); expect(screen.getByText('Bob · 2 replies')).toBeInTheDocument(); }); - it('falls back to the parent message author when the channel has no display title', async () => { - useChannelDisplayName.mockReturnValue(undefined); + it('falls back to the parent message author when the channel has no display title', () => { + useChannelPreviewInfo.mockReturnValue({ displayTitle: undefined }); - await renderComponent({ + renderComponent({ channelOverrides: { state: { members: { @@ -96,10 +142,10 @@ describe('ThreadHeader', () => { expect(screen.getByText('Alice · 2 replies')).toBeInTheDocument(); }); - it('renders only the reply count when no title source is available', async () => { - useChannelDisplayName.mockReturnValue(undefined); + it('renders only the reply count when no title source is available', () => { + useChannelPreviewInfo.mockReturnValue({ displayTitle: undefined }); - await renderComponent({ + renderComponent({ channelOverrides: { state: { members: { @@ -115,4 +161,26 @@ 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', () => { + useChannelPreviewInfo.mockReturnValue({ 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', () => { + useChannelPreviewInfo.mockReturnValue({ displayTitle: 'Bob' }); + + renderComponent({ + activeChatView: 'threads', + threadContext: { id: 'thread-1' }, + }); + + expect(screen.getByTestId('toggle-sidebar-button')).toBeInTheDocument(); + }); });