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
68 changes: 43 additions & 25 deletions examples/vite/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,12 @@ body {
}

.str-chat__channel-list {
flex: 0 0 300px;
max-width: 300px;
height: 100%;

&.str-chat__channel-list--open {
flex: 0 0 300px;
max-width: 300px;
}
}

.str-chat__main-panel {
Expand Down Expand Up @@ -89,42 +92,57 @@ body {
//max-width: none;
}

.str-chat__dropzone-root--thread,
.str-chat__thread-list-container,
.str-chat__thread-container {
//flex: 0 0 360px;
width: 100%;
max-width: 360px;
}

.str-chat__chat-view__threads {
/* Threads view: thread detail takes all space (higher specificity than channel 360px rules). */
.str-chat__chat-view .str-chat__chat-view__threads {
.str-chat__dropzone-root--thread,
.str-chat__thread-container {
flex: 1 1 auto;
//min-width: 360px;
min-width: 0;
max-width: none;
width: 100%;
}
}

@media (max-width: 1100px) {
.str-chat__container:has(.str-chat__thread-container) > .str-chat__main-panel {
width: 0;
min-width: 0;
flex: 0 0 0;
overflow: hidden;
.str-chat__thread-list-container .str-chat__thread-container {
flex: 1 1 auto;
min-width: 0;
width: 100%;
max-width: none;
}

/* Desktop (≥768px): in channel view only, thread fixed 360px next to channel. */
@media (min-width: 768px) {
.str-chat__chat-view__channels .str-chat__container:has(.str-chat__thread-container) {
> .str-chat__main-panel,
> .str-chat__dropzone-root:not(.str-chat__dropzone-root--thread) {
flex: 1 1 auto;
min-width: 0;
}

> .str-chat__thread-container,
> .str-chat__dropzone-root--thread {
flex: 0 0 360px;
width: 360px;
max-width: 360px;
}
}

.str-chat__container:has(.str-chat__thread-container) > .str-chat__thread-container {
flex: 1 1 auto;
min-width: 360px;
max-width: none;
.str-chat__chat-view__channels .str-chat__container .str-chat__dropzone-root--thread,
.str-chat__chat-view__channels .str-chat__container .str-chat__thread-container {
width: 100%;
max-width: 360px;
}

.str-chat__thread-list-container.str-chat__thread-list-container--open {
width: 100%;
max-width: 360px;
}
}

@container (max-width: 860px) {
.str-chat__channel-list,
.str-chat__chat-view__selector {
display: none;
.str-chat__thread-container {
width: 100%;
max-width: initial;
}
}
}
35 changes: 35 additions & 0 deletions src/components/Button/ToggleSidebarButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useIsMobileViewport } from '../ChannelHeader/hooks/useIsMobileViewport';
import { useChatContext, useTranslationContext } from '../../context';
import { Button, type ButtonProps } from './Button';

type ToggleSidebarButtonProps = ButtonProps & {
/** expand mode is usually assigned to button, whose task is to show the sidebar, and collapse vice versa */
mode: 'expand' | 'collapse';
/** usually can collapse if an item from sidebar was selected */
canCollapse?: boolean;
};

export const ToggleSidebarButton = ({
canCollapse,
mode,
...props
}: ToggleSidebarButtonProps) => {
const { closeMobileNav, navOpen, openMobileNav } = useChatContext('ChannelHeader');
const { t } = useTranslationContext('ChannelHeader');
const toggleNav = navOpen ? closeMobileNav : openMobileNav;
const isMobileViewport = useIsMobileViewport();
const showButton = mode === 'expand' ? isMobileViewport || !navOpen : canCollapse;

return showButton ? (
<Button
appearance='ghost'
aria-label={navOpen ? t('aria/Collapse sidebar') : t('aria/Expand sidebar')}
circular
className='str-chat__header-sidebar-toggle'
onClick={toggleNav}
size='md'
variant='secondary'
{...props}
/>
) : null;
};
21 changes: 21 additions & 0 deletions src/components/Channel/styling/Channel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,27 @@
width: 100%;
min-width: 0;
}

/* Mobile (<768px): when thread is open, show only thread (channel panel collapsed). */
@media (max-width: 767px) {
&:has(.str-chat__thread-container) {
> .str-chat__main-panel,
> .str-chat__dropzone-root:not(.str-chat__dropzone-root--thread) {
flex: 0 0 0;
width: 0;
min-width: 0;
max-width: 0;
overflow: hidden;
}

> .str-chat__thread-container,
> .str-chat__dropzone-root--thread {
flex: 1 1 auto;
min-width: 0;
width: 100%;
}
}
}
}
}

Expand Down
29 changes: 8 additions & 21 deletions src/components/ChannelHeader/ChannelHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
import React from 'react';

import { IconLayoutAlignLeft } from '../Icons/icons';
import type { ChannelAvatarProps } from '../Avatar';
import { Avatar as DefaultAvatar } from '../Avatar';
import { useChannelHeaderOnlineStatus } from './hooks/useChannelHeaderOnlineStatus';
import { useChannelPreviewInfo } from '../ChannelPreview/hooks/useChannelPreviewInfo';
import { useChannelStateContext } from '../../context/ChannelStateContext';
import { useChatContext } from '../../context/ChatContext';
import { useTranslationContext } from '../../context/TranslationContext';
import type { ChannelAvatarProps } from '../Avatar';
import { Button } from '../Button';
import clsx from 'clsx';
import { ToggleSidebarButton } from '../Button/ToggleSidebarButton';

export type ChannelHeaderProps = {
/** UI component to display an avatar, defaults to [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.tsx) component and accepts the same props as: [ChannelAvatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/ChannelAvatar.tsx) */
Avatar?: React.ComponentType<ChannelAvatarProps>;
/** Manually set the image to render, defaults to the Channel image */
image?: string;
/** UI component to display menu icon, defaults to [MenuIcon](https://github.com/GetStream/stream-chat-react/blob/master/src/components/ChannelHeader/ChannelHeader.tsx)*/
/** UI component to display menu icon, defaults to IconLayoutAlignLeft*/
MenuIcon?: React.ComponentType;
/** When true, shows IconLayoutAlignLeft instead of MenuIcon for sidebar expansion */
sidebarCollapsed?: boolean;
/** Set title manually */
title?: string;
};
Expand All @@ -32,13 +29,11 @@ export const ChannelHeader = (props: ChannelHeaderProps) => {
Avatar = DefaultAvatar,
image: overrideImage,
MenuIcon = IconLayoutAlignLeft,
sidebarCollapsed = true,
title: overrideTitle,
} = props;

const { channel } = useChannelStateContext();
const { openMobileNav } = useChatContext('ChannelHeader');
const { t } = useTranslationContext('ChannelHeader');
const { navOpen } = useChatContext('ChannelHeader');
const { displayImage, displayTitle, groupChannelDisplayInfo } = useChannelPreviewInfo({
channel,
overrideImage,
Expand All @@ -49,20 +44,12 @@ export const ChannelHeader = (props: ChannelHeaderProps) => {
return (
<div
className={clsx('str-chat__channel-header', {
'str-chat__channel-header--sidebar-collapsed': sidebarCollapsed,
'str-chat__channel-header--sidebar-collapsed': !navOpen,
})}
>
<Button
appearance='ghost'
aria-label={sidebarCollapsed ? t('aria/Expand sidebar') : t('aria/Menu')}
circular
className='str-chat__header-sidebar-toggle'
onClick={openMobileNav}
size='md'
variant='secondary'
>
{sidebarCollapsed && <MenuIcon />}
</Button>
<ToggleSidebarButton mode='expand'>
<MenuIcon />
</ToggleSidebarButton>
<div className='str-chat__channel-header__data'>
<div className='str-chat__channel-header__data__title'>{displayTitle}</div>
{onlineStatusText != null && (
Expand Down
24 changes: 24 additions & 0 deletions src/components/ChannelHeader/hooks/useIsMobileViewport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect, useState } from 'react';

import { NAV_SIDEBAR_DESKTOP_BREAKPOINT } from '../../Chat/hooks/useChat';

const mobileQuery = () =>
typeof window !== 'undefined'
? window.matchMedia(`(max-width: ${NAV_SIDEBAR_DESKTOP_BREAKPOINT - 1}px)`)
: null;

/** True when viewport width is below NAV_SIDEBAR_DESKTOP_BREAKPOINT (768px). */
export const useIsMobileViewport = (): boolean => {
const [isMobile, setIsMobile] = useState(() => mobileQuery()?.matches ?? false);

useEffect(() => {
const mql = mobileQuery();
if (!mql) return;
const handler = () => setIsMobile(mql.matches);
handler();
mql.addEventListener('change', handler);
return () => mql.removeEventListener('change', handler);
}, []);

return isMobile;
};
3 changes: 3 additions & 0 deletions src/components/ChannelHeader/styling/ChannelHeader.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
@use '../../../styling/utils';

.str-chat {
--str-chat__channel-header-height: 72px;

/* The border radius used for the borders of the component */
--str-chat__channel-header-border-radius: 0;

Expand Down Expand Up @@ -37,6 +39,7 @@
align-items: center;
flex: 1;
min-width: 0;
height: var(--str-chat__channel-header-height);

.str-chat__channel-header__data {
@include utils.header-text-layout;
Expand Down
4 changes: 3 additions & 1 deletion src/components/ChannelList/ChannelList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import type { ChannelAvatarProps } from '../Avatar';
import type { TranslationContextValue } from '../../context/TranslationContext';
import type { PaginatorProps } from '../../types/types';
import type { LoadingErrorIndicatorProps } from '../Loading';
import { ChannelListHeader } from './ChannelListHeader';

const DEFAULT_FILTERS = {};
const DEFAULT_OPTIONS = {};
Expand Down Expand Up @@ -210,7 +211,7 @@ const UnMemoizedChannelList = (props: ChannelListProps) => {
client,
closeMobileNav,
customClasses,
navOpen = false,
navOpen = true,
searchController,
setActiveChannel,
theme,
Expand Down Expand Up @@ -382,6 +383,7 @@ const UnMemoizedChannelList = (props: ChannelListProps) => {
value={{ channels, hasNextPage, loadNextPage, setChannels }}
>
<div className={className} ref={channelListRef}>
<ChannelListHeader />
{showChannelSearch &&
(Search ? (
<Search
Expand Down
28 changes: 28 additions & 0 deletions src/components/ChannelList/ChannelListHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { type ComponentType } from 'react';
import clsx from 'clsx';
import { useChatContext, useTranslationContext } from '../../context';
import { IconLayoutAlignLeft } from '../Icons';
import { ToggleSidebarButton } from '../Button/ToggleSidebarButton';

export type ChannelListHeaderProps = {
ToggleButtonIcon?: ComponentType;
};

export const ChannelListHeader = ({
ToggleButtonIcon = IconLayoutAlignLeft,
}: ChannelListHeaderProps) => {
const { t } = useTranslationContext();
const { channel, navOpen } = useChatContext();
return (
<div
className={clsx('str-chat__channel-list__header', {
'str-chat__channel-list__header--sidebar-collapsed': !navOpen,
})}
>
<div className='str-chat__channel-list__header__title'>{t('Chats')}</div>
<ToggleSidebarButton canCollapse={!!channel} mode={'collapse'}>
<ToggleButtonIcon />
</ToggleSidebarButton>
</div>
);
};
5 changes: 5 additions & 0 deletions src/components/ChannelList/hooks/useMobileNavigation.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { useEffect } from 'react';

const MOBILE_NAV_BREAKPOINT = 768;

export const useMobileNavigation = (
channelListRef: React.RefObject<HTMLDivElement | null>,
navOpen: boolean,
closeMobileNav?: () => void,
) => {
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (typeof window !== 'undefined' && window.innerWidth >= MOBILE_NAV_BREAKPOINT) {
return;
}
if (
closeMobileNav &&
channelListRef.current &&
Expand Down
27 changes: 27 additions & 0 deletions src/components/ChannelList/styling/ChannelList.scss
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,31 @@
@include utils.empty-theme('channel-list');
color: var(--str-chat__channel-list-empty-indicator-color);
}

/* Mobile: hide when nav closed; when open show as overlay. */
@media (max-width: 767px) {
display: none;

&.str-chat__channel-list--open {
display: flex;
position: absolute;
top: 0;
bottom: 0;
z-index: 1;
min-width: 280px;
max-width: 100%;
box-shadow: var(--str-chat__channel-list-box-shadow);
}
}

/* Desktop (≥768px): collapse when nav closed so main content uses space. */
@media (min-width: 768px) {
&:not(.str-chat__channel-list--open) {
flex: 0 0 0;
width: 0;
min-width: 0;
max-width: 0;
overflow: hidden;
}
}
}
22 changes: 22 additions & 0 deletions src/components/ChannelList/styling/ChannelListHeader.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.str-chat__channel-list__header {
display: flex;
align-items: center;
padding: var(--spacing-md);
height: var(--str-chat__channel-header-height);
width: 100%;

.str-chat__channel-list__header__title {
flex: 1;
font: var(--str-chat__heading-lg-text);
color: var(--text-primary);
}

&.str-chat__channel-list__header--sidebar-collapsed {
flex: 0 0 0;
width: 0;
min-width: 0;
max-width: 0;
overflow: hidden;
padding: 0;
}
}
Loading
Loading