Skip to content

Commit 8d25ead

Browse files
authored
fix: reliably detect whether the click originated inside before closing mobile nav (#3061)
1 parent 6804ed6 commit 8d25ead

7 files changed

Lines changed: 524 additions & 174 deletions

File tree

examples/vite/src/App.tsx

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -181,23 +181,49 @@ const App = () => {
181181
const { tokenProvider, userId, userImage, userName } = useUser();
182182
const chatView = useAppSettingsSelector((state) => state.chatView);
183183
const { mode: themeMode } = useAppSettingsSelector((state) => state.theme);
184+
const initialSearchParams = useMemo(
185+
() => new URLSearchParams(window.location.search),
186+
[],
187+
);
184188
const initialChannelId = useMemo(() => getSelectedChannelIdFromUrl(), []);
185189
const initialChatView = useMemo(() => getSelectedChatViewFromUrl(), []);
186-
const initialThreadOpen = useMemo(
187-
() => Boolean(new URLSearchParams(window.location.search).get('thread')),
188-
[],
190+
const initialThreadId = useMemo(
191+
() => initialSearchParams.get('thread'),
192+
[initialSearchParams],
189193
);
194+
const initialThreadOpen = useMemo(() => Boolean(initialThreadId), [initialThreadId]);
190195
const initialPanelLayout = useMemo(
191196
() => appSettingsStore.getLatestValue().panelLayout,
192197
[],
193198
);
194-
const initialNavOpen = useMemo(
195-
() =>
196-
typeof window !== 'undefined' && window.innerWidth < DESKTOP_LAYOUT_BREAKPOINT
197-
? true
198-
: !initialPanelLayout.leftPanel.collapsed,
199-
[initialPanelLayout.leftPanel.collapsed],
200-
);
199+
const initialNavOpen = useMemo(() => {
200+
if (typeof window === 'undefined') return !initialPanelLayout.leftPanel.collapsed;
201+
202+
const isMobile = window.innerWidth < DESKTOP_LAYOUT_BREAKPOINT;
203+
204+
if (!isMobile) return !initialPanelLayout.leftPanel.collapsed;
205+
206+
const hasSelectedChannel = Boolean(initialChannelId);
207+
const hasSelectedThread = Boolean(initialThreadId);
208+
const channelsView = initialChatView !== 'threads';
209+
210+
// Keep sidebar open on mobile when a channel is preselected via URL.
211+
// It will auto-collapse later once the selected channel is actually resolved.
212+
if (channelsView && hasSelectedChannel) {
213+
return true;
214+
}
215+
216+
if ((!channelsView && hasSelectedThread) || hasSelectedThread) {
217+
return false;
218+
}
219+
220+
return true;
221+
}, [
222+
initialChannelId,
223+
initialChatView,
224+
initialPanelLayout.leftPanel.collapsed,
225+
initialThreadId,
226+
]);
201227
const appLayoutRef = useRef<HTMLDivElement | null>(null);
202228

203229
const chatClient = useCreateChatClient({
@@ -298,8 +324,8 @@ const App = () => {
298324
return (
299325
<LoadingScreen
300326
initialAppLayoutStyle={initialAppLayoutStyle}
327+
initialChannelSelected={Boolean(initialChannelId)}
301328
initialNavOpen={initialNavOpen}
302-
initialThreadOpen={initialThreadOpen}
303329
/>
304330
);
305331
}
@@ -358,17 +384,18 @@ const App = () => {
358384
<ChatView>
359385
<ChatStateSync initialChatView={initialChatView} />
360386
<SidebarLayoutSync />
361-
<ChatView.Selector
362-
itemSet={chatViewSelectorItemSet}
363-
iconOnly={chatView.iconOnly}
364-
/>
365387
<ChannelsPanels
366388
filters={filters}
389+
iconOnly={chatView.iconOnly}
367390
initialChannelId={initialChannelId ?? undefined}
391+
itemSet={chatViewSelectorItemSet}
368392
options={options}
369393
sort={sort}
370394
/>
371-
<ThreadsPanels />
395+
<ThreadsPanels
396+
iconOnly={chatView.iconOnly}
397+
itemSet={chatViewSelectorItemSet}
398+
/>
372399
</ChatView>
373400
</div>
374401
</Chat>

examples/vite/src/AppSettings/state.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ export type NotificationsSettingsState = {
1919
verticalAlignment: 'bottom' | 'top';
2020
};
2121

22-
export const LEFT_PANEL_MIN_WIDTH = 360;
23-
export const THREAD_PANEL_MIN_WIDTH = 360;
22+
export const LEFT_PANEL_MIN_WIDTH = 260;
23+
export const THREAD_PANEL_MIN_WIDTH = 260;
2424

2525
export type LeftPanelLayoutSettingsState = {
2626
collapsed: boolean;

examples/vite/src/ChatLayout/Panels.tsx

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
WithDragAndDropUpload,
1919
useChannelStateContext,
2020
useChatContext,
21+
type ChatViewSelectorEntry,
22+
useThreadsViewContext,
2123
} from 'stream-chat-react';
2224

2325
import { SidebarResizeHandle, ThreadResizeHandle } from './Resize.tsx';
@@ -41,80 +43,114 @@ const ChannelThreadPanel = () => {
4143
);
4244
};
4345

46+
const ResponsiveChannelPanels = () => {
47+
const { thread } = useChannelStateContext('ResponsiveChannelPanels');
48+
const isThreadOpen = !!thread;
49+
50+
return (
51+
<div
52+
className={clsx('app-chat-view__channel-content', {
53+
'app-chat-view__channel-content--thread-open': isThreadOpen,
54+
})}
55+
>
56+
<WithDragAndDropUpload className='app-chat-view__channel-main'>
57+
<Window>
58+
<ChannelHeader Avatar={ChannelAvatar} />
59+
<MessageList returnAllReadData />
60+
<AIStateIndicator />
61+
<MessageComposer
62+
focus
63+
audioRecordingEnabled
64+
maxRows={10}
65+
asyncMessagesMultiSendEnabled
66+
/>
67+
</Window>
68+
</WithDragAndDropUpload>
69+
<ChannelThreadPanel />
70+
</div>
71+
);
72+
};
73+
4474
export const ChannelsPanels = ({
4575
filters,
76+
iconOnly,
4677
initialChannelId,
78+
itemSet,
4779
options,
4880
sort,
4981
}: {
5082
filters: ChannelFilters;
83+
iconOnly?: boolean;
5184
initialChannelId?: string;
85+
itemSet?: ChatViewSelectorEntry[];
5286
options: ChannelOptions;
5387
sort: ChannelSort;
5488
}) => {
55-
const { navOpen = true } = useChatContext('ChannelsPanels');
89+
const { channel, navOpen = true } = useChatContext('ChannelsPanels');
5690
const channelsLayoutRef = useRef<HTMLDivElement | null>(null);
5791

5892
return (
5993
<ChatView.Channels>
6094
<div
6195
className={clsx('app-chat-view__channels-layout', {
96+
'app-chat-view__channels-layout--channel-selected': !!channel?.id,
6297
'app-chat-view__channels-layout--sidebar-collapsed': navOpen === false,
6398
})}
6499
ref={channelsLayoutRef}
65100
>
66-
<WithComponents
67-
overrides={{
68-
// @ts-expect-error TODO: adjust the sizing
69-
Avatar: ChannelAvatar,
70-
}}
71-
>
72-
<ChannelList
73-
customActiveChannel={initialChannelId}
74-
filters={filters}
75-
options={options}
76-
sort={sort}
77-
showChannelSearch
78-
/>
79-
</WithComponents>
101+
<div className='app-chat-sidebar-overlay'>
102+
<ChatView.Selector iconOnly={iconOnly} itemSet={itemSet} />
103+
<WithComponents
104+
overrides={{
105+
// @ts-expect-error TODO: adjust the sizing
106+
Avatar: ChannelAvatar,
107+
}}
108+
>
109+
<ChannelList
110+
customActiveChannel={initialChannelId}
111+
filters={filters}
112+
options={options}
113+
sort={sort}
114+
showChannelSearch
115+
/>
116+
</WithComponents>
117+
</div>
80118
<SidebarResizeHandle layoutRef={channelsLayoutRef} />
81119
<WithComponents overrides={{ TypingIndicator }}>
82120
<Channel>
83-
<WithDragAndDropUpload>
84-
<Window>
85-
<ChannelHeader Avatar={ChannelAvatar} />
86-
<MessageList returnAllReadData />
87-
<AIStateIndicator />
88-
<MessageComposer
89-
focus
90-
audioRecordingEnabled
91-
maxRows={10}
92-
asyncMessagesMultiSendEnabled
93-
/>
94-
</Window>
95-
</WithDragAndDropUpload>
96-
<ChannelThreadPanel />
121+
<ResponsiveChannelPanels />
97122
</Channel>
98123
</WithComponents>
99124
</div>
100125
</ChatView.Channels>
101126
);
102127
};
103128

104-
export const ThreadsPanels = () => {
129+
export const ThreadsPanels = ({
130+
iconOnly,
131+
itemSet,
132+
}: {
133+
iconOnly?: boolean;
134+
itemSet?: ChatViewSelectorEntry[];
135+
}) => {
105136
const { navOpen = true } = useChatContext('ThreadsPanels');
137+
const { activeThread } = useThreadsViewContext();
106138
const threadsLayoutRef = useRef<HTMLDivElement | null>(null);
107139

108140
return (
109141
<ChatView.Threads>
110142
<ThreadStateSync />
111143
<div
112144
className={clsx('app-chat-view__threads-layout', {
145+
'app-chat-view__threads-layout--thread-selected': !!activeThread?.id,
113146
'app-chat-view__threads-layout--sidebar-collapsed': navOpen === false,
114147
})}
115148
ref={threadsLayoutRef}
116149
>
117-
<ThreadList />
150+
<div className='app-chat-sidebar-overlay'>
151+
<ChatView.Selector iconOnly={iconOnly} itemSet={itemSet} />
152+
<ThreadList />
153+
</div>
118154
<SidebarResizeHandle layoutRef={threadsLayoutRef} />
119155
<div className='app-chat-view__threads-main'>
120156
<ChatView.ThreadAdapter>

examples/vite/src/LoadingScreen/LoadingScreen.tsx

Lines changed: 32 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,42 +9,52 @@ import { LoadingChannel, LoadingChannels } from 'stream-chat-react';
99
// Update this layout every time layout in App.tsx is updated.
1010
type LoadingScreenProps = {
1111
initialAppLayoutStyle: CSSProperties;
12+
initialChannelSelected: boolean;
1213
initialNavOpen: boolean;
13-
initialThreadOpen: boolean;
1414
};
1515

1616
const selectorButtonCount = 4;
1717

1818
export const LoadingScreen = ({
1919
initialAppLayoutStyle,
20+
initialChannelSelected,
2021
initialNavOpen,
21-
initialThreadOpen,
2222
}: LoadingScreenProps) => (
2323
<div className='app-chat-layout' style={initialAppLayoutStyle}>
2424
<div className='str-chat'>
2525
<div className='str-chat__chat-view'>
26-
<div
27-
className={clsx('str-chat__chat-view__selector', {
28-
'str-chat__chat-view__selector--nav-closed': !initialNavOpen,
29-
'str-chat__chat-view__selector--nav-open': initialNavOpen,
30-
})}
31-
>
32-
{Array.from({ length: selectorButtonCount }).map((_, index) => (
33-
<div className='str-chat__chat-view__selector-button-container' key={index}>
34-
<div className='str-chat__chat-view__selector-button'>
35-
<span className='str-chat__loading-channels-avatar' />
36-
</div>
37-
</div>
38-
))}
39-
</div>
4026
<div className='str-chat__chat-view__channels'>
4127
<div
4228
className={clsx('app-chat-view__channels-layout', {
29+
'app-chat-view__channels-layout--channel-selected': initialChannelSelected,
4330
'app-chat-view__channels-layout--sidebar-collapsed': !initialNavOpen,
4431
})}
4532
>
46-
<div className='str-chat__channel-list'>
47-
<LoadingChannels />
33+
<div className='app-chat-sidebar-overlay'>
34+
<div
35+
className={clsx('str-chat__chat-view__selector', {
36+
'str-chat__chat-view__selector--nav-closed': !initialNavOpen,
37+
'str-chat__chat-view__selector--nav-open': initialNavOpen,
38+
})}
39+
>
40+
{Array.from({ length: selectorButtonCount }).map((_, index) => (
41+
<div
42+
className='str-chat__chat-view__selector-button-container'
43+
key={index}
44+
>
45+
<div className='str-chat__chat-view__selector-button'>
46+
<span className='str-chat__loading-channels-avatar' />
47+
</div>
48+
</div>
49+
))}
50+
</div>
51+
<div
52+
className={clsx('str-chat__channel-list', {
53+
'str-chat__channel-list--open': initialNavOpen,
54+
})}
55+
>
56+
<LoadingChannels />
57+
</div>
4858
</div>
4959
<div
5060
aria-orientation='vertical'
@@ -56,38 +66,12 @@ export const LoadingScreen = ({
5666
</div>
5767
</div>
5868
<div className='str-chat__channel'>
59-
<div className='str-chat__container'>
60-
<div className='str-chat__main-panel'>
61-
<div className='str-chat__main-panel-inner'>
62-
<div className='str-chat__window'>
63-
<LoadingChannel />
64-
</div>
69+
<div className='str-chat__main-panel'>
70+
<div className='str-chat__main-panel-inner'>
71+
<div className='str-chat__window app-loading-screen__window'>
72+
<LoadingChannel />
6573
</div>
6674
</div>
67-
<div
68-
aria-orientation='vertical'
69-
className={clsx(
70-
'app-chat-resize-handle app-chat-resize-handle--thread',
71-
{
72-
'app-chat-resize-handle--thread-hidden': !initialThreadOpen,
73-
},
74-
)}
75-
role='separator'
76-
>
77-
<div className='app-chat-resize-handle__hitbox'>
78-
<div className='app-chat-resize-handle__line' />
79-
</div>
80-
</div>
81-
<div
82-
className={clsx(
83-
'str-chat__dropzone-root--thread app-chat-thread-panel',
84-
{
85-
'app-chat-thread-panel--open': initialThreadOpen,
86-
},
87-
)}
88-
>
89-
<LoadingChannel />
90-
</div>
9175
</div>
9276
</div>
9377
</div>

0 commit comments

Comments
 (0)