From 6bd51e4745be028a3cc4fb9633b0c61eb057fce5 Mon Sep 17 00:00:00 2001 From: martincupela Date: Thu, 30 Apr 2026 15:56:25 +0200 Subject: [PATCH 1/5] chore(demo): add PublicChannelOverlay --- examples/vite/src/ChatLayout/Panels.tsx | 2 + .../PublicChannelOverlay.scss | 52 +++++++++++++++++++ .../PublicChannelOverlay.tsx | 51 ++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.scss create mode 100644 examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.tsx diff --git a/examples/vite/src/ChatLayout/Panels.tsx b/examples/vite/src/ChatLayout/Panels.tsx index 9a1159a2f..e78474a55 100644 --- a/examples/vite/src/ChatLayout/Panels.tsx +++ b/examples/vite/src/ChatLayout/Panels.tsx @@ -34,6 +34,7 @@ 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 { PublicChannelOverlay } from '../PublicChannelOverlay/PublicChannelOverlay.tsx'; import { useSidebar } from './SidebarContext.tsx'; import { ThreadStateSync } from './Sync.tsx'; @@ -97,6 +98,7 @@ const ResponsiveChannelPanels = () => { maxRows={10} asyncMessagesMultiSendEnabled /> + diff --git a/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.scss b/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.scss new file mode 100644 index 000000000..bcee76822 --- /dev/null +++ b/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.scss @@ -0,0 +1,52 @@ +.app-public-channel-overlay { + position: absolute; + inset: 0; + z-index: 1; + 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 { + display: flex; + flex-direction: column; + align-items: center; + padding: 40px 16px; + + .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; +} diff --git a/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.tsx b/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.tsx new file mode 100644 index 000000000..5a5e15316 --- /dev/null +++ b/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.tsx @@ -0,0 +1,51 @@ +import { useCallback } from 'react'; +import type { ChannelMemberResponse } from 'stream-chat'; +import { + Button, + IconMessageBubbles, + useChannelMembersState, + useChannelStateContext, + useChatContext, +} from 'stream-chat-react'; + +import './PublicChannelOverlay.scss'; + +export const PublicChannelOverlay = () => { + 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 handleJoin = useCallback(() => { + channel.addMembers([client.userID!]); + }, [channel, client.userID]); + + if (isMember) return null; + + return ( +
+
+ +
+

+ You're previewing this group +

+

+ Join to send messages and follow the conversation +

+
+ +
+
+ ); +}; From f197d4fa646da138861df1d10dcd1aeb48c75dcd Mon Sep 17 00:00:00 2001 From: martincupela Date: Thu, 30 Apr 2026 12:12:42 +0200 Subject: [PATCH 2/5] chore(demo): fix the filters --- examples/vite/src/App.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/examples/vite/src/App.tsx b/examples/vite/src/App.tsx index b511bf8c0..5be3e02ae 100644 --- a/examples/vite/src/App.tsx +++ b/examples/vite/src/App.tsx @@ -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] }, + }, + ], }, ], }), From 6b8b4b7af4837297cd79450468ca5abd604c6341 Mon Sep 17 00:00:00 2001 From: martincupela Date: Thu, 30 Apr 2026 16:00:42 +0200 Subject: [PATCH 3/5] chore(demo): do not cover ChannelHeader with PublicChannelOverlay --- examples/vite/src/ChatLayout/Panels.tsx | 34 +++++++++++++------------ examples/vite/src/index.scss | 8 ++++++ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/examples/vite/src/ChatLayout/Panels.tsx b/examples/vite/src/ChatLayout/Panels.tsx index e78474a55..1d773ab78 100644 --- a/examples/vite/src/ChatLayout/Panels.tsx +++ b/examples/vite/src/ChatLayout/Panels.tsx @@ -83,22 +83,24 @@ const ResponsiveChannelPanels = () => { - {messageListType === 'virtualized' ? ( - - ) : ( - - )} - - - - +
+ {messageListType === 'virtualized' ? ( + + ) : ( + + )} + + + + +
diff --git a/examples/vite/src/index.scss b/examples/vite/src/index.scss index 16da70747..850b18236 100644 --- a/examples/vite/src/index.scss +++ b/examples/vite/src/index.scss @@ -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; From c023d8b6dae67e60b804ec4a01cf5af8806543c8 Mon Sep 17 00:00:00 2001 From: martincupela Date: Thu, 30 Apr 2026 16:33:57 +0200 Subject: [PATCH 4/5] chore(demo): add reactivity to joining a channel --- examples/vite/src/ChatLayout/Panels.tsx | 41 ++----------------- .../PublicChannelOverlay.scss | 37 ++++++++++++++++- .../PublicChannelOverlay.tsx | 35 +++++++++++++--- 3 files changed, 68 insertions(+), 45 deletions(-) diff --git a/examples/vite/src/ChatLayout/Panels.tsx b/examples/vite/src/ChatLayout/Panels.tsx index 1d773ab78..6b200684f 100644 --- a/examples/vite/src/ChatLayout/Panels.tsx +++ b/examples/vite/src/ChatLayout/Panels.tsx @@ -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, @@ -26,8 +21,6 @@ import { useChatContext, type ChatViewSelectorEntry, useThreadsViewContext, - Button, - useChannelMembersState, } from 'stream-chat-react'; import { useAppSettingsSelector } from '../AppSettings/state'; @@ -108,34 +101,6 @@ const ResponsiveChannelPanels = () => { ); }; -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 ( - - ); -}; - export const ChannelsPanels = ({ filters, iconOnly, @@ -197,7 +162,7 @@ export const ChannelsPanels = ({ - + diff --git a/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.scss b/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.scss index bcee76822..8bb855964 100644 --- a/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.scss +++ b/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.scss @@ -15,10 +15,36 @@ } .app-public-channel-overlay__content { + position: relative; display: flex; flex-direction: column; align-items: center; - padding: 40px 16px; + 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; @@ -50,3 +76,12 @@ 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; + } +} diff --git a/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.tsx b/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.tsx index 5a5e15316..7a8672d63 100644 --- a/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.tsx +++ b/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.tsx @@ -1,11 +1,13 @@ -import { useCallback } from 'react'; +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'; @@ -15,14 +17,34 @@ export const PublicChannelOverlay = () => { const { channel } = useChannelStateContext(); const members = useChannelMembersState(channel); const membership = members[client.userID!] as ChannelMemberResponse | undefined; + const { addNotification } = useNotificationApi(); + const [joining, setJoining] = useState(false); const isMember = typeof membership?.channel_role === 'string'; + const canJoin = channel.data?.own_capabilities?.includes('join-channel'); - const handleJoin = useCallback(() => { - channel.addMembers([client.userID!]); - }, [channel, client.userID]); + 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) return null; + if (isMember || !canJoin) return null; return (
@@ -39,11 +61,12 @@ export const PublicChannelOverlay = () => {
From eeefd87cd9743d9a538574ad8e7e75145199ee7f Mon Sep 17 00:00:00 2001 From: martincupela Date: Mon, 4 May 2026 12:31:35 +0200 Subject: [PATCH 5/5] chore(demo): show channel to read only if cannot join --- examples/vite/src/ChatLayout/Panels.tsx | 32 +++++++++++++------ .../PublicChannelOverlay.scss | 19 ++++++++++- .../PublicChannelOverlay.tsx | 26 +++++++++++++-- examples/vite/src/index.scss | 7 +++- 4 files changed, 70 insertions(+), 14 deletions(-) diff --git a/examples/vite/src/ChatLayout/Panels.tsx b/examples/vite/src/ChatLayout/Panels.tsx index 6b200684f..99ab7b2cc 100644 --- a/examples/vite/src/ChatLayout/Panels.tsx +++ b/examples/vite/src/ChatLayout/Panels.tsx @@ -27,7 +27,11 @@ 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 { PublicChannelOverlay } from '../PublicChannelOverlay/PublicChannelOverlay.tsx'; +import { + PublicChannelComposerBanner, + PublicChannelOverlay, + usePublicChannelState, +} from '../PublicChannelOverlay/PublicChannelOverlay.tsx'; import { useSidebar } from './SidebarContext.tsx'; import { ThreadStateSync } from './Sync.tsx'; @@ -62,6 +66,23 @@ const ChannelThreadPanel = () => { ); }; +const MessageComposerOrBanner = () => { + const { canJoin, isMember } = usePublicChannelState(); + + if (!isMember && !canJoin) return ; + + return ( + + ); +}; + const ResponsiveChannelPanels = () => { const { thread } = useChannelStateContext('ResponsiveChannelPanels'); const isThreadOpen = !!thread; @@ -84,14 +105,7 @@ const ResponsiveChannelPanels = () => { )} - + diff --git a/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.scss b/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.scss index 8bb855964..225cdabc5 100644 --- a/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.scss +++ b/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.scss @@ -1,7 +1,7 @@ .app-public-channel-overlay { position: absolute; inset: 0; - z-index: 1; + z-index: 3; display: flex; align-items: center; justify-content: center; @@ -85,3 +85,20 @@ 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; +} diff --git a/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.tsx b/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.tsx index 7a8672d63..126b60b50 100644 --- a/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.tsx +++ b/examples/vite/src/PublicChannelOverlay/PublicChannelOverlay.tsx @@ -12,17 +12,23 @@ import { import './PublicChannelOverlay.scss'; -export const PublicChannelOverlay = () => { +export const usePublicChannelState = () => { const { client } = useChatContext(); const { channel } = useChannelStateContext(); const members = useChannelMembersState(channel); const membership = members[client.userID!] as ChannelMemberResponse | undefined; - const { addNotification } = useNotificationApi(); - const [joining, setJoining] = useState(false); 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 { @@ -72,3 +78,17 @@ export const PublicChannelOverlay = () => { ); }; + +export const PublicChannelComposerBanner = () => { + const { canJoin, isMember } = usePublicChannelState(); + + if (isMember || canJoin) return null; + + return ( +
+

+ You can only view this conversation +

+
+ ); +}; diff --git a/examples/vite/src/index.scss b/examples/vite/src/index.scss index 850b18236..14faf0ae6 100644 --- a/examples/vite/src/index.scss +++ b/examples/vite/src/index.scss @@ -225,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) {