Skip to content
Merged
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
17 changes: 12 additions & 5 deletions examples/vite/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -291,11 +291,18 @@ const App = () => {
{ type: 'public' },
// public example channels
{
cid: {
$in: ['random', 'general', 'music', 'jokes'].map(
(channelId) => `messaging:${channelId}`,
),
},
$and: [
{
cid: {
$in: ['random', 'general', 'music', 'jokes'].map(
(channelId) => `messaging:${channelId}`,
),
},
},
{
members: { $in: [userId] },
},
],
},
],
}),
Expand Down
89 changes: 36 additions & 53 deletions examples/vite/src/ChatLayout/Panels.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import clsx from 'clsx';
import type {
ChannelFilters,
ChannelMemberResponse,
ChannelOptions,
ChannelSort,
} from 'stream-chat';
import { useCallback, useEffect, useRef } from 'react';
import type { ChannelFilters, ChannelOptions, ChannelSort } from 'stream-chat';
import { useEffect, useRef } from 'react';
import {
AIStateIndicator,
Channel,
Expand All @@ -26,14 +21,17 @@ import {
useChatContext,
type ChatViewSelectorEntry,
useThreadsViewContext,
Button,
useChannelMembersState,
} from 'stream-chat-react';

import { useAppSettingsSelector } from '../AppSettings/state';
import { DESKTOP_LAYOUT_BREAKPOINT } from './constants.ts';
import { SidebarResizeHandle, ThreadResizeHandle } from './Resize.tsx';
import { ReturnToSkipNavigation } from '../AccessibilityNavigation/ReturnToSkipNavigation.tsx';
import {
PublicChannelComposerBanner,
PublicChannelOverlay,
usePublicChannelState,
} from '../PublicChannelOverlay/PublicChannelOverlay.tsx';
import { useSidebar } from './SidebarContext.tsx';
import { ThreadStateSync } from './Sync.tsx';

Expand Down Expand Up @@ -68,6 +66,23 @@ const ChannelThreadPanel = () => {
);
};

const MessageComposerOrBanner = () => {
const { canJoin, isMember } = usePublicChannelState();

if (!isMember && !canJoin) return <PublicChannelComposerBanner />;

return (
<MessageComposer
additionalTextareaProps={{
id: CHANNEL_MESSAGE_COMPOSER_TEXTAREA_TARGET_ID,
}}
audioRecordingEnabled
maxRows={10}
asyncMessagesMultiSendEnabled
/>
);
};

const ResponsiveChannelPanels = () => {
const { thread } = useChannelStateContext('ResponsiveChannelPanels');
const isThreadOpen = !!thread;
Expand All @@ -82,56 +97,24 @@ const ResponsiveChannelPanels = () => {
<WithDragAndDropUpload className='app-chat-view__channel-main'>
<Window>
<ChannelHeader Avatar={ChannelAvatar} />
{messageListType === 'virtualized' ? (
<VirtualizedMessageList returnAllReadData shouldGroupByUser />
) : (
<MessageList returnAllReadData />
)}
<ReturnToSkipNavigation />
<AIStateIndicator />
<MessageComposer
additionalTextareaProps={{
id: CHANNEL_MESSAGE_COMPOSER_TEXTAREA_TARGET_ID,
}}
audioRecordingEnabled
maxRows={10}
asyncMessagesMultiSendEnabled
/>
<div className='app-chat-view__channel-body'>
{messageListType === 'virtualized' ? (
<VirtualizedMessageList returnAllReadData shouldGroupByUser />
) : (
<MessageList returnAllReadData />
)}
<ReturnToSkipNavigation />
<AIStateIndicator />
<MessageComposerOrBanner />
<PublicChannelOverlay />
</div>
</Window>
</WithDragAndDropUpload>
<ChannelThreadPanel />
</div>
);
};

const HeaderStartContent = () => {
const { client } = useChatContext();
const { channel } = useChannelStateContext();
const members = useChannelMembersState(channel);
const membership = members[client.userID!] as ChannelMemberResponse | undefined;

const isMember = typeof membership?.channel_role === 'string';
const canJoin = channel.data?.own_capabilities?.includes('join-channel');

const handleClick = useCallback(() => {
if (isMember) {
channel.removeMembers([client.userID!]).then(() => {
channel.watch();
});
} else {
channel.addMembers([client.userID!]);
}
}, [isMember]);

if (!canJoin) return null;

return (
<Button onClick={handleClick} variant='secondary' appearance='outline' size='sm'>
{isMember ? 'Leave' : 'Join'}
</Button>
);
};

export const ChannelsPanels = ({
filters,
iconOnly,
Expand Down Expand Up @@ -193,7 +176,7 @@ export const ChannelsPanels = ({
</WithComponents>
</div>
<SidebarResizeHandle layoutRef={channelsLayoutRef} />
<WithComponents overrides={{ TypingIndicator, HeaderStartContent }}>
<WithComponents overrides={{ TypingIndicator }}>
<Channel>
<ResponsiveChannelPanels />
</Channel>
Expand Down
104 changes: 104 additions & 0 deletions examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
.app-public-channel-overlay {
position: absolute;
inset: 0;
z-index: 3;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(5px);
background: rgba(255, 255, 255, 0.75);
padding: 12px 0;

.str-chat__theme-dark & {
background: rgba(0, 0, 0, 0.75);
}
}

.app-public-channel-overlay__content {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 48px;
overscroll-behavior: contain;

&::before {
content: '';
position: absolute;
inset: -120px;
border-radius: 50%;
background: radial-gradient(
circle,
rgba(255, 255, 255, 0.9) 0%,
rgba(255, 255, 255, 0.85) 40%,
rgba(255, 255, 255, 0) 70%
);
filter: blur(20px);
z-index: -1;

.str-chat__theme-dark & {
background: radial-gradient(
circle,
rgba(0, 0, 0, 0.6) 0%,
rgba(0, 0, 0, 0.55) 40%,
rgba(0, 0, 0, 0) 70%
);
}
}

.str-chat__icon {
width: 32px;
height: 32px;
color: var(--str-chat__text-tertiary);
}
}

.app-public-channel-overlay__text {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: var(--str-chat__spacing-xxs);
margin-block: var(--str-chat__spacing-sm) var(--str-chat__spacing-xl);

p {
margin: 0;
}
}

.app-public-channel-overlay__title {
font: var(--str-chat__font-heading-xs);
color: var(--str-chat__text-color);
}

.app-public-channel-overlay__description {
font: var(--str-chat__font-caption-default);
color: var(--str-chat__text-secondary);
max-width: 200px;
}

.app-public-channel-overlay__join-button {
width: 106px;

.str-chat__loading-indicator {
height: 20px;
width: 20px;
}
}

.app-public-channel-composer-banner {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 56px;
padding: 16px;
border-top: 1px solid var(--str-chat__border-color);
}

.app-public-channel-composer-banner__text {
margin: 0;
font: var(--str-chat__font-caption-default);
color: var(--str-chat__text-secondary);
text-align: center;
}
94 changes: 94 additions & 0 deletions examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useCallback, useState } from 'react';
import type { ChannelMemberResponse } from 'stream-chat';
import {
Button,
IconMessageBubbles,
LoadingIndicator,
useChannelMembersState,
useChannelStateContext,
useChatContext,
useNotificationApi,
} from 'stream-chat-react';

import './PublicChannelOverlay.scss';

export const usePublicChannelState = () => {
const { client } = useChatContext();
const { channel } = useChannelStateContext();
const members = useChannelMembersState(channel);
const membership = members[client.userID!] as ChannelMemberResponse | undefined;

const isMember = typeof membership?.channel_role === 'string';
const canJoin = channel.data?.own_capabilities?.includes('join-channel');

return { canJoin, channel, client, isMember };
};

export const PublicChannelOverlay = () => {
const { canJoin, channel, client, isMember } = usePublicChannelState();
const { addNotification } = useNotificationApi();
const [joining, setJoining] = useState(false);

const handleJoin = useCallback(async () => {
setJoining(true);
try {
await channel.addMembers([client.userID!]);
} catch (error) {
addNotification({
emitter: 'PublicChannelOverlay',
incident: {
domain: 'api',
entity: 'channel',
operation: 'join',
},
message: 'Failed to join the group',
severity: 'error',
error: error instanceof Error ? error : new Error(String(error)),
});
} finally {
setJoining(false);
}
}, [addNotification, channel, client.userID]);

if (isMember || !canJoin) return null;

return (
<div className='app-public-channel-overlay'>
<div className='app-public-channel-overlay__content'>
<IconMessageBubbles />
<div className='app-public-channel-overlay__text'>
<p className='app-public-channel-overlay__title'>
You're previewing this group
</p>
<p className='app-public-channel-overlay__description'>
Join to send messages and follow the conversation
</p>
</div>
<Button
appearance='solid'
className='app-public-channel-overlay__join-button'
disabled={joining}
onClick={handleJoin}
size='md'
variant='primary'
>
{joining ? <LoadingIndicator /> : 'Join Group'}
</Button>
</div>
</div>
);
};

export const PublicChannelComposerBanner = () => {
const { canJoin, isMember } = usePublicChannelState();

if (isMember || canJoin) return null;

return (
<div className='app-public-channel-composer-banner'>
<p className='app-public-channel-composer-banner__text'>
You can only view this conversation
</p>
</div>
);
};
15 changes: 14 additions & 1 deletion examples/vite/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,14 @@ body {
height: 100%;
}

.app-chat-view__channel-body {
position: relative;
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
}

.app-chat-view__threads-main > * {
flex: 1 1 auto;
min-width: 0;
Expand Down Expand Up @@ -217,7 +225,12 @@ body {
}

.str-chat__tooltip {
z-index: 10;
z-index: 2;
}

.str-chat__notification-list,
.str-chat__dialog-overlay {
z-index: 4;
}

@media (max-width: 767px) {
Expand Down