Skip to content

Commit 76228d8

Browse files
authored
chore(demo): add channel preview overlay (#3157)
1 parent a6e1bdc commit 76228d8

5 files changed

Lines changed: 260 additions & 59 deletions

File tree

examples/vite/src/App.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -291,11 +291,18 @@ const App = () => {
291291
{ type: 'public' },
292292
// public example channels
293293
{
294-
cid: {
295-
$in: ['random', 'general', 'music', 'jokes'].map(
296-
(channelId) => `messaging:${channelId}`,
297-
),
298-
},
294+
$and: [
295+
{
296+
cid: {
297+
$in: ['random', 'general', 'music', 'jokes'].map(
298+
(channelId) => `messaging:${channelId}`,
299+
),
300+
},
301+
},
302+
{
303+
members: { $in: [userId] },
304+
},
305+
],
299306
},
300307
],
301308
}),

examples/vite/src/ChatLayout/Panels.tsx

Lines changed: 36 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import clsx from 'clsx';
2-
import type {
3-
ChannelFilters,
4-
ChannelMemberResponse,
5-
ChannelOptions,
6-
ChannelSort,
7-
} from 'stream-chat';
8-
import { useCallback, useEffect, useRef } from 'react';
2+
import type { ChannelFilters, ChannelOptions, ChannelSort } from 'stream-chat';
3+
import { useEffect, useRef } from 'react';
94
import {
105
AIStateIndicator,
116
Channel,
@@ -26,14 +21,17 @@ import {
2621
useChatContext,
2722
type ChatViewSelectorEntry,
2823
useThreadsViewContext,
29-
Button,
30-
useChannelMembersState,
3124
} from 'stream-chat-react';
3225

3326
import { useAppSettingsSelector } from '../AppSettings/state';
3427
import { DESKTOP_LAYOUT_BREAKPOINT } from './constants.ts';
3528
import { SidebarResizeHandle, ThreadResizeHandle } from './Resize.tsx';
3629
import { ReturnToSkipNavigation } from '../AccessibilityNavigation/ReturnToSkipNavigation.tsx';
30+
import {
31+
PublicChannelComposerBanner,
32+
PublicChannelOverlay,
33+
usePublicChannelState,
34+
} from '../PublicChannelOverlay/PublicChannelOverlay.tsx';
3735
import { useSidebar } from './SidebarContext.tsx';
3836
import { ThreadStateSync } from './Sync.tsx';
3937

@@ -68,6 +66,23 @@ const ChannelThreadPanel = () => {
6866
);
6967
};
7068

69+
const MessageComposerOrBanner = () => {
70+
const { canJoin, isMember } = usePublicChannelState();
71+
72+
if (!isMember && !canJoin) return <PublicChannelComposerBanner />;
73+
74+
return (
75+
<MessageComposer
76+
additionalTextareaProps={{
77+
id: CHANNEL_MESSAGE_COMPOSER_TEXTAREA_TARGET_ID,
78+
}}
79+
audioRecordingEnabled
80+
maxRows={10}
81+
asyncMessagesMultiSendEnabled
82+
/>
83+
);
84+
};
85+
7186
const ResponsiveChannelPanels = () => {
7287
const { thread } = useChannelStateContext('ResponsiveChannelPanels');
7388
const isThreadOpen = !!thread;
@@ -82,56 +97,24 @@ const ResponsiveChannelPanels = () => {
8297
<WithDragAndDropUpload className='app-chat-view__channel-main'>
8398
<Window>
8499
<ChannelHeader Avatar={ChannelAvatar} />
85-
{messageListType === 'virtualized' ? (
86-
<VirtualizedMessageList returnAllReadData shouldGroupByUser />
87-
) : (
88-
<MessageList returnAllReadData />
89-
)}
90-
<ReturnToSkipNavigation />
91-
<AIStateIndicator />
92-
<MessageComposer
93-
additionalTextareaProps={{
94-
id: CHANNEL_MESSAGE_COMPOSER_TEXTAREA_TARGET_ID,
95-
}}
96-
audioRecordingEnabled
97-
maxRows={10}
98-
asyncMessagesMultiSendEnabled
99-
/>
100+
<div className='app-chat-view__channel-body'>
101+
{messageListType === 'virtualized' ? (
102+
<VirtualizedMessageList returnAllReadData shouldGroupByUser />
103+
) : (
104+
<MessageList returnAllReadData />
105+
)}
106+
<ReturnToSkipNavigation />
107+
<AIStateIndicator />
108+
<MessageComposerOrBanner />
109+
<PublicChannelOverlay />
110+
</div>
100111
</Window>
101112
</WithDragAndDropUpload>
102113
<ChannelThreadPanel />
103114
</div>
104115
);
105116
};
106117

107-
const HeaderStartContent = () => {
108-
const { client } = useChatContext();
109-
const { channel } = useChannelStateContext();
110-
const members = useChannelMembersState(channel);
111-
const membership = members[client.userID!] as ChannelMemberResponse | undefined;
112-
113-
const isMember = typeof membership?.channel_role === 'string';
114-
const canJoin = channel.data?.own_capabilities?.includes('join-channel');
115-
116-
const handleClick = useCallback(() => {
117-
if (isMember) {
118-
channel.removeMembers([client.userID!]).then(() => {
119-
channel.watch();
120-
});
121-
} else {
122-
channel.addMembers([client.userID!]);
123-
}
124-
}, [isMember]);
125-
126-
if (!canJoin) return null;
127-
128-
return (
129-
<Button onClick={handleClick} variant='secondary' appearance='outline' size='sm'>
130-
{isMember ? 'Leave' : 'Join'}
131-
</Button>
132-
);
133-
};
134-
135118
export const ChannelsPanels = ({
136119
filters,
137120
iconOnly,
@@ -193,7 +176,7 @@ export const ChannelsPanels = ({
193176
</WithComponents>
194177
</div>
195178
<SidebarResizeHandle layoutRef={channelsLayoutRef} />
196-
<WithComponents overrides={{ TypingIndicator, HeaderStartContent }}>
179+
<WithComponents overrides={{ TypingIndicator }}>
197180
<Channel>
198181
<ResponsiveChannelPanels />
199182
</Channel>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
.app-public-channel-overlay {
2+
position: absolute;
3+
inset: 0;
4+
z-index: 3;
5+
display: flex;
6+
align-items: center;
7+
justify-content: center;
8+
backdrop-filter: blur(5px);
9+
background: rgba(255, 255, 255, 0.75);
10+
padding: 12px 0;
11+
12+
.str-chat__theme-dark & {
13+
background: rgba(0, 0, 0, 0.75);
14+
}
15+
}
16+
17+
.app-public-channel-overlay__content {
18+
position: relative;
19+
display: flex;
20+
flex-direction: column;
21+
align-items: center;
22+
padding: 40px 48px;
23+
overscroll-behavior: contain;
24+
25+
&::before {
26+
content: '';
27+
position: absolute;
28+
inset: -120px;
29+
border-radius: 50%;
30+
background: radial-gradient(
31+
circle,
32+
rgba(255, 255, 255, 0.9) 0%,
33+
rgba(255, 255, 255, 0.85) 40%,
34+
rgba(255, 255, 255, 0) 70%
35+
);
36+
filter: blur(20px);
37+
z-index: -1;
38+
39+
.str-chat__theme-dark & {
40+
background: radial-gradient(
41+
circle,
42+
rgba(0, 0, 0, 0.6) 0%,
43+
rgba(0, 0, 0, 0.55) 40%,
44+
rgba(0, 0, 0, 0) 70%
45+
);
46+
}
47+
}
48+
49+
.str-chat__icon {
50+
width: 32px;
51+
height: 32px;
52+
color: var(--str-chat__text-tertiary);
53+
}
54+
}
55+
56+
.app-public-channel-overlay__text {
57+
display: flex;
58+
flex-direction: column;
59+
align-items: center;
60+
text-align: center;
61+
gap: var(--str-chat__spacing-xxs);
62+
margin-block: var(--str-chat__spacing-sm) var(--str-chat__spacing-xl);
63+
64+
p {
65+
margin: 0;
66+
}
67+
}
68+
69+
.app-public-channel-overlay__title {
70+
font: var(--str-chat__font-heading-xs);
71+
color: var(--str-chat__text-color);
72+
}
73+
74+
.app-public-channel-overlay__description {
75+
font: var(--str-chat__font-caption-default);
76+
color: var(--str-chat__text-secondary);
77+
max-width: 200px;
78+
}
79+
80+
.app-public-channel-overlay__join-button {
81+
width: 106px;
82+
83+
.str-chat__loading-indicator {
84+
height: 20px;
85+
width: 20px;
86+
}
87+
}
88+
89+
.app-public-channel-composer-banner {
90+
display: flex;
91+
align-items: center;
92+
justify-content: center;
93+
width: 100%;
94+
min-height: 56px;
95+
padding: 16px;
96+
border-top: 1px solid var(--str-chat__border-color);
97+
}
98+
99+
.app-public-channel-composer-banner__text {
100+
margin: 0;
101+
font: var(--str-chat__font-caption-default);
102+
color: var(--str-chat__text-secondary);
103+
text-align: center;
104+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { useCallback, useState } from 'react';
2+
import type { ChannelMemberResponse } from 'stream-chat';
3+
import {
4+
Button,
5+
IconMessageBubbles,
6+
LoadingIndicator,
7+
useChannelMembersState,
8+
useChannelStateContext,
9+
useChatContext,
10+
useNotificationApi,
11+
} from 'stream-chat-react';
12+
13+
import './PublicChannelOverlay.scss';
14+
15+
export const usePublicChannelState = () => {
16+
const { client } = useChatContext();
17+
const { channel } = useChannelStateContext();
18+
const members = useChannelMembersState(channel);
19+
const membership = members[client.userID!] as ChannelMemberResponse | undefined;
20+
21+
const isMember = typeof membership?.channel_role === 'string';
22+
const canJoin = channel.data?.own_capabilities?.includes('join-channel');
23+
24+
return { canJoin, channel, client, isMember };
25+
};
26+
27+
export const PublicChannelOverlay = () => {
28+
const { canJoin, channel, client, isMember } = usePublicChannelState();
29+
const { addNotification } = useNotificationApi();
30+
const [joining, setJoining] = useState(false);
31+
32+
const handleJoin = useCallback(async () => {
33+
setJoining(true);
34+
try {
35+
await channel.addMembers([client.userID!]);
36+
} catch (error) {
37+
addNotification({
38+
emitter: 'PublicChannelOverlay',
39+
incident: {
40+
domain: 'api',
41+
entity: 'channel',
42+
operation: 'join',
43+
},
44+
message: 'Failed to join the group',
45+
severity: 'error',
46+
error: error instanceof Error ? error : new Error(String(error)),
47+
});
48+
} finally {
49+
setJoining(false);
50+
}
51+
}, [addNotification, channel, client.userID]);
52+
53+
if (isMember || !canJoin) return null;
54+
55+
return (
56+
<div className='app-public-channel-overlay'>
57+
<div className='app-public-channel-overlay__content'>
58+
<IconMessageBubbles />
59+
<div className='app-public-channel-overlay__text'>
60+
<p className='app-public-channel-overlay__title'>
61+
You're previewing this group
62+
</p>
63+
<p className='app-public-channel-overlay__description'>
64+
Join to send messages and follow the conversation
65+
</p>
66+
</div>
67+
<Button
68+
appearance='solid'
69+
className='app-public-channel-overlay__join-button'
70+
disabled={joining}
71+
onClick={handleJoin}
72+
size='md'
73+
variant='primary'
74+
>
75+
{joining ? <LoadingIndicator /> : 'Join Group'}
76+
</Button>
77+
</div>
78+
</div>
79+
);
80+
};
81+
82+
export const PublicChannelComposerBanner = () => {
83+
const { canJoin, isMember } = usePublicChannelState();
84+
85+
if (isMember || canJoin) return null;
86+
87+
return (
88+
<div className='app-public-channel-composer-banner'>
89+
<p className='app-public-channel-composer-banner__text'>
90+
You can only view this conversation
91+
</p>
92+
</div>
93+
);
94+
};

examples/vite/src/index.scss

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,14 @@ body {
138138
height: 100%;
139139
}
140140

141+
.app-chat-view__channel-body {
142+
position: relative;
143+
display: flex;
144+
flex-direction: column;
145+
flex: 1 1 auto;
146+
min-height: 0;
147+
}
148+
141149
.app-chat-view__threads-main > * {
142150
flex: 1 1 auto;
143151
min-width: 0;
@@ -217,7 +225,12 @@ body {
217225
}
218226

219227
.str-chat__tooltip {
220-
z-index: 10;
228+
z-index: 2;
229+
}
230+
231+
.str-chat__notification-list,
232+
.str-chat__dialog-overlay {
233+
z-index: 4;
221234
}
222235

223236
@media (max-width: 767px) {

0 commit comments

Comments
 (0)