Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
233c446
feat: add ChannelDetail component
MartinCupela Jun 2, 2026
66ad542
Merge remote-tracking branch 'origin/master' into feat/channel-detail
MartinCupela Jun 2, 2026
cc84438
refactor: use ListItemLayout for switch fields in ChannelInfoActions.…
MartinCupela Jun 2, 2026
07d83a6
Merge remote-tracking branch 'origin/master' into feat/channel-detail
MartinCupela Jun 3, 2026
c0c9fc2
feat(ChannelManagementView): invoke confirmation dialogs before invok…
MartinCupela Jun 4, 2026
7054821
feat: add useIsUserMuted hook
MartinCupela Jun 4, 2026
6e87676
fix: route notifications through generated modal dialogs
MartinCupela Jun 4, 2026
d2ad28f
feat: allow to unblock user from ChannelManagementView
MartinCupela Jun 4, 2026
679fd6b
feat: add edit mode to ChannelManagementView
MartinCupela Jun 5, 2026
f226e61
feat: add ChannelMembersView
MartinCupela Jun 9, 2026
5195112
fix: close context menu rendered above modal
MartinCupela Jun 9, 2026
d133b38
chore(demo): allow to configure channel detail actions
MartinCupela Jun 9, 2026
4cc1aaa
chore(demo): migrate app settings modal to SectionNavigator
MartinCupela Jun 9, 2026
e84e32c
fix: extract role correctly
MartinCupela Jun 9, 2026
bd205e6
fix: prevent channel members views lists re-render on pagination
MartinCupela Jun 10, 2026
4f40ee2
feat(ChannelDetail): add PinnedMessageView
MartinCupela Jun 10, 2026
5f111a3
feat(ChannelDetail): add ChannelMediaView and ChannelFilesView
MartinCupela Jun 11, 2026
563364f
feat(ChannelDetail): fix bugs, refactor
MartinCupela Jun 12, 2026
34cd0c9
feat(ChannelDetail): fix bugs, refactor
MartinCupela Jun 12, 2026
9a97fd5
feat(ChannelDetail): wrap ListItemLayout content in a container
MartinCupela Jun 12, 2026
d8ef308
refactor(ChannelDetail): move AvatarWithChannelDetail to Avatar folder
MartinCupela Jun 12, 2026
c7352f4
Merge remote-tracking branch 'origin/master' into feat/channel-detail
MartinCupela Jun 12, 2026
53236f7
refactor(ChannelDetail): reduce the index export for Avatar styles
MartinCupela Jun 12, 2026
97a07bf
refactor(ChannelDetail): convert ChannelDetail into plugin
MartinCupela Jun 12, 2026
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
2 changes: 1 addition & 1 deletion examples/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port 5175",
"dev": "vite --host --port 5173",
"build": "tsc && vite build",
"preview": "vite preview"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
IconClock,
IconExclamationMark,
IconExclamationTriangleFill,
IconInfo,
IconMinus,
IconPlusSmall,
IconRefresh,
Expand Down Expand Up @@ -47,7 +48,7 @@ const severityIcons: Partial<
Record<NotificationSeverity, React.ComponentType<{ className?: string }>>
> = {
error: IconExclamationMark,
info: IconExclamationMark,
info: IconInfo,
loading: IconRefresh,
success: IconCheckmark,
warning: IconExclamationTriangleFill,
Expand Down
113 changes: 77 additions & 36 deletions examples/vite/src/AppSettings/AppSettings.scss
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@
.app__notification-dialog__duration-controls {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;

.str-chat__form-input-field {
Expand Down Expand Up @@ -968,37 +969,25 @@
height: min(80vh, 760px);
background: var(--str-chat__background-core-elevation-2);
color: var(--str-chat__text-primary);
border: 1px solid var(--str-chat__border-core-default);
border-radius: 14px;
}

.app__settings-modal__header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 16px 20px;
font-size: 1.5rem;
font-weight: 700;
border-bottom: 1px solid var(--str-chat__border-core-default);

svg.str-chat__icon--cog {
height: 1.75rem;
width: 1.75rem;
}
overflow: hidden;
}

.app__settings-modal__body {
display: grid;
grid-template-columns: minmax(180px, 240px) minmax(0, 1fr);
min-height: 0;
height: 100%;
}

.app__settings-modal__tabs {
.app__settings-modal__body .str-chat__section-navigator__navigation {
overflow-y: auto;
overscroll-behavior: contain;
border-inline-end: 1px solid var(--str-chat__border-core-default);
padding: 10px;
width: auto;
}

.app__settings-modal__body .str-chat__section-navigator__navigation-item {
padding: 0;
}

.app__settings-modal__tab {
Expand All @@ -1018,22 +1007,51 @@
font-weight: 600;
}

.app__settings-modal__content {
overflow-y: auto;
overscroll-behavior: contain;
padding: 20px 24px;
.app__settings-modal__content-stack {
display: flex;
flex-direction: column;
}

.app__settings-modal__content-stack {
.app__settings-modal__tab-header .str-chat__prompt__header__title {
color: var(--str-chat__text-primary);
font: var(--str-chat__font-heading-sm);
margin: 0;
}

.app__settings-modal__tab-header .str-chat__prompt__header__description {
color: var(--str-chat__text-secondary);
font: var(--str-chat__font-caption-default);
margin: 0;
}

.app__settings-modal__tab-header .str-chat__prompt__header__trailing-content {
display: flex;
align-items: center;
gap: var(--str-chat__spacing-xs);
}

.app__settings-modal__tab-header .str-chat__prompt__header__close-button {
flex-shrink: 0;
color: var(--str-chat__text-primary);
}

.app__settings-modal__tab-header
.str-chat__prompt__header__close-button
.str-chat__icon {
height: var(--str-chat__icon-size-sm);
width: var(--str-chat__icon-size-sm);
}

.app__settings-modal__tab-body {
display: flex;
flex-direction: column;
gap: 20px;
gap: var(--str-chat__spacing-xl);
}

.app__settings-modal__field {
display: flex;
flex-direction: column;
gap: 10px;
gap: var(--str-chat__spacing-xs);
}

.app__settings-modal__field-label {
Expand All @@ -1059,6 +1077,22 @@
flex-wrap: wrap;
}

.app__settings-modal__action-list {
display: flex;
flex-direction: column;
gap: 10px;
}

.app__settings-modal__action-row {
display: grid;
grid-template-columns: minmax(180px, 1fr) auto;
gap: 12px;
align-items: center;
border: 1px solid var(--str-chat__border-core-default);
border-radius: 10px;
padding: 10px 12px;
}

.app__settings-modal__option-button[aria-pressed='true'] {
border-color: var(--str-chat__border-utility-selected);
background: var(--str-chat__background-utility-selected);
Expand Down Expand Up @@ -1117,16 +1151,7 @@
flex-direction: column;
}

.app__settings-modal {
width: min(92vw, 680px);
}

.app__settings-modal__body {
grid-template-columns: minmax(140px, 180px) minmax(0, 1fr);
}

.app__settings-modal__tabs {
border-inline-end: 1px solid var(--str-chat__border-core-default);
.app__settings-modal__body .str-chat__section-navigator__navigation {
border-bottom: 0;
display: block;
gap: 0;
Expand All @@ -1135,4 +1160,20 @@
overflow-x: hidden;
}
}

.app__settings-modal--inline {
width: 100dvw;
height: 100dvh;
max-width: none;
border-radius: 0;
box-shadow: none;

.app__settings-modal__tab-header {
padding: var(--str-chat__spacing-xs);
}

.app__settings-modal__tab-body {
padding: var(--str-chat__spacing-lg);
}
}
}
158 changes: 109 additions & 49 deletions examples/vite/src/AppSettings/AppSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import React, { type ComponentType, useState } from 'react';
import { type ComponentType, useCallback, useMemo, useState } from 'react';
import type { SectionNavigatorLayout } from 'stream-chat-react';
import {
Button,
ChatViewSelectorButton,
GlobalModal,
IconBell,
IconEmoji,
IconMessageBubble,
IconMessageBubbles,
SECTION_NAVIGATOR_LAYOUT,
SectionNavigator,
type SectionNavigatorNavButtonProps,
type SectionNavigatorSection,
} from 'stream-chat-react';

import { ActionsMenu } from './ActionsMenu';
import { ChannelDetailTab } from './tabs/ChannelDetail';
import { GeneralTab } from './tabs/General';
import { MessageActionsTab } from './tabs/MessageActions';
import { NotificationsTab } from './tabs/Notifications';
Expand All @@ -22,17 +29,94 @@ import {
IconSun,
IconTextDirection,
} from '../icons.tsx';
import clsx from 'clsx';

type TabId = 'general' | 'messageActions' | 'notifications' | 'reactions' | 'sidebar';
type TabId =
| 'channelDetail'
| 'general'
| 'messageActions'
| 'notifications'
| 'reactions'
| 'sidebar';

const tabConfig: { Icon: ComponentType; id: TabId; title: string }[] = [
{ Icon: IconGear, id: 'general', title: 'General' },
{ Icon: IconMessageBubble, id: 'messageActions', title: 'Message Actions' },
{ Icon: IconBell, id: 'notifications', title: 'Notifications' },
{ Icon: IconSidebar, id: 'sidebar', title: 'Sidebar' },
{ Icon: IconEmoji, id: 'reactions', title: 'Reactions' },
type SettingsSectionConfig = {
Content: ComponentType<SettingsTabContentProps>;
Icon: ComponentType;
id: TabId;
title: string;
};

type SettingsTabContentProps = {
close: () => void;
};

const settingsSectionConfig: SettingsSectionConfig[] = [
{ Content: GeneralTab, Icon: IconGear, id: 'general', title: 'General' },
{
Content: ChannelDetailTab,
Icon: IconMessageBubbles,
id: 'channelDetail',
title: 'Channel Detail',
},
{
Content: MessageActionsTab,
Icon: IconMessageBubble,
id: 'messageActions',
title: 'Message Actions',
},
{
Content: NotificationsTab,
Icon: IconBell,
id: 'notifications',
title: 'Notifications',
},
{ Content: SidebarTab, Icon: IconSidebar, id: 'sidebar', title: 'Sidebar' },
{ Content: ReactionsTab, Icon: IconEmoji, id: 'reactions', title: 'Reactions' },
];

const createSettingsNavButton = ({
Icon,
id,
title,
}: Pick<SettingsSectionConfig, 'Icon' | 'id' | 'title'>) => {
const SettingsNavButton = ({ select, selected }: SectionNavigatorNavButtonProps) => (
<Button
aria-selected={selected}
className={`app__settings-modal__tab str-chat__button--ghost str-chat__button--secondary str-chat__button--size-lg ${
selected ? 'app__settings-modal__tab--active' : ''
}`}
onClick={select}
role='tab'
>
<Icon />
{title}
</Button>
);
SettingsNavButton.displayName = `${id}SettingsNavButton`;

return SettingsNavButton;
};

const createSettingsSectionContent = ({
close,
Content,
id,
}: Pick<SettingsSectionConfig, 'Content' | 'id'> & {
close: () => void;
}) => {
const SettingsSectionContent = () => <Content close={close} />;
SettingsSectionContent.displayName = `${id}SettingsSectionContent`;

return SettingsSectionContent;
};

const createSettingsSections = (close: () => void): SectionNavigatorSection[] =>
settingsSectionConfig.map(({ Content, Icon, id, title }) => ({
id,
NavButton: createSettingsNavButton({ Icon, id, title }),
SectionContent: createSettingsSectionContent({ close, Content, id }),
}));

const SidebarThemeToggle = ({ iconOnly = true }: { iconOnly?: boolean }) => {
const {
theme,
Expand Down Expand Up @@ -88,8 +172,13 @@ const SidebarRtlToggle = ({ iconOnly = true }: { iconOnly?: boolean }) => {
};

export const AppSettings = ({ iconOnly = true }: { iconOnly?: boolean }) => {
const [activeTab, setActiveTab] = useState<TabId>('general');
const [open, setOpen] = useState(false);
const closeSettingsModal = useCallback(() => setOpen(false), []);
const settingsSections = useMemo(
() => createSettingsSections(closeSettingsModal),
[closeSettingsModal],
);
const [layout, setLayout] = useState<SectionNavigatorLayout | undefined>();

return (
<div className='app__settings-group'>
Expand All @@ -103,46 +192,17 @@ export const AppSettings = ({ iconOnly = true }: { iconOnly?: boolean }) => {
onClick={() => setOpen(true)}
text='Settings'
/>
<GlobalModal onClose={() => setOpen(false)} open={open}>
<div className='app__settings-modal'>
<header className='app__settings-modal__header'>
<IconGear />
Settings
</header>
<div className='app__settings-modal__body'>
<nav
aria-label='Settings sections'
className='app__settings-modal__tabs'
role='tablist'
>
{tabConfig.map(({ Icon, id, title }) => (
<Button
aria-controls={`${id}-content`}
aria-selected={activeTab === id}
className={`app__settings-modal__tab str-chat__button--ghost str-chat__button--secondary str-chat__button--size-lg ${
activeTab === id ? 'app__settings-modal__tab--active' : ''
}`}
key={id}
onClick={() => setActiveTab(id)}
role='tab'
>
<Icon />
{title}
</Button>
))}
</nav>
<section
className='app__settings-modal__content'
id={`${activeTab}-content`}
role='tabpanel'
>
{activeTab === 'general' && <GeneralTab />}
{activeTab === 'messageActions' && <MessageActionsTab />}
{activeTab === 'notifications' && <NotificationsTab />}
{activeTab === 'sidebar' && <SidebarTab />}
{activeTab === 'reactions' && <ReactionsTab />}
</section>
</div>
<GlobalModal onClose={closeSettingsModal} open={open}>
<div
className={clsx('app__settings-modal', {
'app__settings-modal--inline': layout === SECTION_NAVIGATOR_LAYOUT.inline,
})}
>
<SectionNavigator
className='app__settings-modal__body'
sections={settingsSections}
onLayoutChange={setLayout}
/>
</div>
</GlobalModal>
</div>
Expand Down
Loading
Loading