Skip to content

Commit 53d5639

Browse files
authored
fix: show ToggleSidebarButton in threads view's ThreadHeader (#3020)
1 parent 83dec93 commit 53d5639

File tree

2 files changed

+102
-22
lines changed

2 files changed

+102
-22
lines changed

src/components/Thread/ThreadHeader.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import { useTypingContext } from '../../context/TypingContext';
1212
import type { LocalMessage } from 'stream-chat';
1313
import type { ThreadState } from 'stream-chat';
1414
import { Button } from '../Button';
15-
import { IconCrossMedium } from '../Icons';
15+
import { IconCrossMedium, IconLayoutAlignLeft } from '../Icons';
16+
import { ToggleSidebarButton } from '../Button/ToggleSidebarButton';
17+
import { useChatViewContext } from '../ChatView';
1618

1719
const threadStateSelector = ({ replyCount }: ThreadState) => ({ replyCount });
1820

@@ -40,8 +42,10 @@ const ThreadHeaderSubtitle = ({
4042
({ parent_id, user }) => user?.id !== client.user?.id && parent_id === parentId,
4143
);
4244
const hasTyping = channelConfig?.typing_events !== false && typingInThread.length > 0;
43-
const defaultSubtitle =
44-
threadDisplayName + ' · ' + t('replyCount', { count: replyCount ?? 0 });
45+
const replyCountText = t('replyCount', { count: replyCount ?? 0 });
46+
const defaultSubtitle = threadDisplayName
47+
? `${threadDisplayName} · ${replyCountText}`
48+
: replyCountText;
4549
return (
4650
<div className='str-chat__thread-header-subtitle'>
4751
<span
@@ -63,15 +67,18 @@ export type ThreadHeaderProps = {
6367
closeThread: (event?: React.BaseSyntheticEvent) => void;
6468
/** The thread parent message */
6569
thread: LocalMessage;
70+
/** UI component to display menu icon, defaults to IconLayoutAlignLeft*/
71+
MenuIcon?: React.ComponentType;
6672
/** Override the thread display title */
6773
overrideTitle?: string;
6874
};
6975

7076
export const ThreadHeader = (props: ThreadHeaderProps) => {
71-
const { closeThread, overrideTitle, thread } = props;
77+
const { closeThread, MenuIcon = IconLayoutAlignLeft, overrideTitle, thread } = props;
7278

7379
const { t } = useTranslationContext();
74-
const { channel } = useChannelStateContext('ThreadHeader');
80+
const { channel } = useChannelStateContext();
81+
const { activeChatView } = useChatViewContext();
7582
const { displayTitle: channelDisplayTitle } = useChannelPreviewInfo({ channel });
7683

7784
const threadInstance = useThreadContext();
@@ -93,6 +100,11 @@ export const ThreadHeader = (props: ThreadHeaderProps) => {
93100

94101
return (
95102
<div className='str-chat__thread-header'>
103+
{activeChatView === 'threads' && (
104+
<ToggleSidebarButton canCollapse={!!threadInstance} mode='expand'>
105+
<MenuIcon />
106+
</ToggleSidebarButton>
107+
)}
96108
<div className='str-chat__thread-header-details'>
97109
<div className='str-chat__thread-header-title'>{t('Thread')}</div>
98110
<ThreadHeaderSubtitle

src/components/Thread/__tests__/ThreadHeader.test.js

Lines changed: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,47 @@ import React from 'react';
55
import { ChannelStateProvider } from '../../../context/ChannelStateContext';
66
import { ChatProvider } from '../../../context/ChatContext';
77
import { TranslationProvider } from '../../../context/TranslationContext';
8-
import { useChannelDisplayName } from '../../ChannelPreview/hooks/useChannelDisplayName';
98
import { ThreadHeader } from '../ThreadHeader';
109

11-
jest.mock('../../ChannelPreview/hooks/useChannelDisplayName', () => ({
12-
useChannelDisplayName: jest.fn(),
10+
jest.mock('../../ChannelPreview/hooks/useChannelPreviewInfo', () => ({
11+
useChannelPreviewInfo: jest.fn(() => ({ displayTitle: undefined })),
12+
}));
13+
14+
jest.mock('../../../store', () => ({
15+
useStateStore: jest.fn(() => undefined),
16+
}));
17+
18+
jest.mock('../../../context/TypingContext', () => ({
19+
useTypingContext: jest.fn(() => ({ typing: {} })),
20+
}));
21+
22+
jest.mock('../../TypingIndicator/TypingIndicatorHeader', () => ({
23+
TypingIndicatorHeader: () => <div>Typing...</div>,
24+
}));
25+
26+
jest.mock('../../Button/ToggleSidebarButton', () => ({
27+
ToggleSidebarButton: ({ children }) => (
28+
<div data-testid='toggle-sidebar-button'>{children}</div>
29+
),
1330
}));
1431

1532
jest.mock('../../Threads', () => ({
1633
useThreadContext: jest.fn(() => undefined),
1734
}));
1835

36+
jest.mock('../../ChatView', () => ({
37+
useChatViewContext: jest.fn(() => ({ activeChatView: 'channels' })),
38+
}));
39+
40+
const {
41+
useChannelPreviewInfo,
42+
} = require('../../ChannelPreview/hooks/useChannelPreviewInfo');
43+
const { useChatViewContext } = require('../../ChatView');
44+
const { useThreadContext } = require('../../Threads');
45+
1946
const alice = { id: 'alice', name: 'Alice' };
2047
const bob = { id: 'bob', name: 'Bob' };
48+
2149
const createThread = (user) => ({
2250
id: `${user?.id ?? 'thread'}-message`,
2351
reply_count: 2,
@@ -36,20 +64,38 @@ const createChannel = (overrides = {}) => ({
3664
...overrides,
3765
});
3866

39-
const renderComponent = ({ channelOverrides = {}, props = {} } = {}) => {
40-
const client = { off: jest.fn(), on: jest.fn(), userID: alice.id };
67+
const renderComponent = ({
68+
activeChatView = 'channels',
69+
channelOverrides = {},
70+
props = {},
71+
threadContext = undefined,
72+
} = {}) => {
73+
const client = { off: jest.fn(), on: jest.fn(), user: alice, userID: alice.id };
4174
const thread = createThread(alice);
4275
const channel = createChannel(channelOverrides);
4376

77+
useChatViewContext.mockReturnValue({
78+
activeChatView,
79+
setActiveChatView: jest.fn(),
80+
});
81+
useThreadContext.mockReturnValue(threadContext);
82+
4483
return render(
45-
<ChatProvider value={{ client, latestMessageDatesByChannels: {} }}>
46-
<ChannelStateProvider value={{ channel }}>
84+
<ChatProvider
85+
value={{
86+
client,
87+
closeMobileNav: jest.fn(),
88+
latestMessageDatesByChannels: {},
89+
navOpen: false,
90+
openMobileNav: jest.fn(),
91+
}}
92+
>
93+
<ChannelStateProvider value={{ channel, thread }}>
4794
<TranslationProvider
4895
value={{
4996
t: (key, options) => {
5097
if (key === 'Thread') return 'Thread';
5198
if (key === 'replyCount') return `${options.count} replies`;
52-
if (key === 'Direct message') return 'Direct message';
5399
if (key === 'aria/Close thread') return 'Close thread';
54100

55101
return key;
@@ -69,18 +115,18 @@ describe('ThreadHeader', () => {
69115
jest.clearAllMocks();
70116
});
71117

72-
it('renders the channel display title in the subtitle', async () => {
73-
useChannelDisplayName.mockReturnValue('Bob');
118+
it('renders the channel display title in the subtitle', () => {
119+
useChannelPreviewInfo.mockReturnValue({ displayTitle: 'Bob' });
74120

75-
await renderComponent();
121+
renderComponent();
76122

77123
expect(screen.getByText('Bob · 2 replies')).toBeInTheDocument();
78124
});
79125

80-
it('falls back to the parent message author when the channel has no display title', async () => {
81-
useChannelDisplayName.mockReturnValue(undefined);
126+
it('falls back to the parent message author when the channel has no display title', () => {
127+
useChannelPreviewInfo.mockReturnValue({ displayTitle: undefined });
82128

83-
await renderComponent({
129+
renderComponent({
84130
channelOverrides: {
85131
state: {
86132
members: {
@@ -96,10 +142,10 @@ describe('ThreadHeader', () => {
96142
expect(screen.getByText('Alice · 2 replies')).toBeInTheDocument();
97143
});
98144

99-
it('renders only the reply count when no title source is available', async () => {
100-
useChannelDisplayName.mockReturnValue(undefined);
145+
it('renders only the reply count when no title source is available', () => {
146+
useChannelPreviewInfo.mockReturnValue({ displayTitle: undefined });
101147

102-
await renderComponent({
148+
renderComponent({
103149
channelOverrides: {
104150
state: {
105151
members: {
@@ -115,4 +161,26 @@ describe('ThreadHeader', () => {
115161
expect(screen.getByText('2 replies')).toBeInTheDocument();
116162
expect(screen.queryByText(/^undefined ·/)).not.toBeInTheDocument();
117163
});
164+
165+
it('does not render the sidebar toggle in the channels view', () => {
166+
useChannelPreviewInfo.mockReturnValue({ displayTitle: 'Bob' });
167+
168+
renderComponent({
169+
activeChatView: 'channels',
170+
threadContext: { id: 'thread-1' },
171+
});
172+
173+
expect(screen.queryByTestId('toggle-sidebar-button')).not.toBeInTheDocument();
174+
});
175+
176+
it('renders the sidebar toggle in the threads view', () => {
177+
useChannelPreviewInfo.mockReturnValue({ displayTitle: 'Bob' });
178+
179+
renderComponent({
180+
activeChatView: 'threads',
181+
threadContext: { id: 'thread-1' },
182+
});
183+
184+
expect(screen.getByTestId('toggle-sidebar-button')).toBeInTheDocument();
185+
});
118186
});

0 commit comments

Comments
 (0)