From b64529c510c55b1c146301070ee1cc04745ac7a1 Mon Sep 17 00:00:00 2001 From: martincupela Date: Fri, 6 Mar 2026 16:16:34 +0100 Subject: [PATCH 1/5] refactor: hide sidebar toggle button in ChannelHeader --- .../ChannelHeader/ChannelHeader.tsx | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/components/ChannelHeader/ChannelHeader.tsx b/src/components/ChannelHeader/ChannelHeader.tsx index 4bfb2e321..2133561c7 100644 --- a/src/components/ChannelHeader/ChannelHeader.tsx +++ b/src/components/ChannelHeader/ChannelHeader.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import { IconLayoutAlignLeft } from '../Icons/icons'; +// import { IconLayoutAlignLeft } from '../Icons/icons'; 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 { useChatContext } from '../../context/ChatContext'; +// import { useTranslationContext } from '../../context/TranslationContext'; import type { ChannelAvatarProps } from '../Avatar'; -import { Button } from '../Button'; +// import { Button } from '../Button'; import clsx from 'clsx'; export type ChannelHeaderProps = { @@ -31,14 +31,14 @@ export const ChannelHeader = (props: ChannelHeaderProps) => { const { Avatar = DefaultAvatar, image: overrideImage, - MenuIcon = IconLayoutAlignLeft, + // MenuIcon = IconLayoutAlignLeft, sidebarCollapsed = true, title: overrideTitle, } = props; const { channel } = useChannelStateContext(); - const { openMobileNav } = useChatContext('ChannelHeader'); - const { t } = useTranslationContext('ChannelHeader'); + // const { openMobileNav } = useChatContext('ChannelHeader'); + // const { t } = useTranslationContext('ChannelHeader'); const { displayImage, displayTitle, groupChannelDisplayInfo } = useChannelPreviewInfo({ channel, overrideImage, @@ -52,17 +52,17 @@ export const ChannelHeader = (props: ChannelHeaderProps) => { 'str-chat__channel-header--sidebar-collapsed': sidebarCollapsed, })} > - + {/**/} + {/* {sidebarCollapsed && }*/} + {/**/}
{displayTitle}
{onlineStatusText != null && ( From 54340d9ba91e96ef4ebf0a078de210578c72c9bf Mon Sep 17 00:00:00 2001 From: martincupela Date: Fri, 6 Mar 2026 19:18:35 +0100 Subject: [PATCH 2/5] feat: add toggle sidebar functionality across views and resolutions --- examples/vite/src/index.scss | 61 ++++++++------ src/components/Button/ToggleSidebarButton.tsx | 35 ++++++++ src/components/Channel/styling/Channel.scss | 21 +++++ .../ChannelHeader/ChannelHeader.tsx | 35 +++----- .../hooks/useIsMobileViewport.ts | 24 ++++++ .../ChannelHeader/styling/ChannelHeader.scss | 3 + src/components/ChannelList/ChannelList.tsx | 4 +- .../ChannelList/ChannelListHeader.tsx | 28 +++++++ .../ChannelList/hooks/useMobileNavigation.ts | 5 ++ .../ChannelList/styling/ChannelList.scss | 28 +++++++ .../styling/ChannelListHeader.scss | 22 +++++ src/components/ChannelList/styling/index.scss | 1 + src/components/Chat/Chat.tsx | 13 ++- src/components/Chat/hooks/useChat.ts | 20 ++++- src/components/ChatView/ChatView.tsx | 35 +++++--- src/components/ChatView/styling/ChatView.scss | 28 ++++++- .../MessageInput/styling/MessageComposer.scss | 5 +- .../styling/SendToChannelCheckbox.scss | 20 +++-- .../Notifications/notificationOrigin.ts | 5 ++ .../styling/ResizableContainer.scss | 81 +++++++++++++++++++ src/components/Thread/Thread.tsx | 11 ++- src/components/Thread/ThreadHeaderMain.tsx | 47 +++++++++++ .../Thread/styling/ThreadHeaderMain.scss | 25 ++++++ src/components/Thread/styling/index.scss | 1 + .../Threads/ThreadList/ThreadList.tsx | 11 ++- .../Threads/ThreadList/ThreadListHeader.tsx | 30 +++++++ .../Threads/ThreadList/ThreadListItemUI.tsx | 16 +++- .../ThreadList/styling/ThreadList.scss | 28 +++++++ .../ThreadList/styling/ThreadListHeader.scss | 26 ++++++ .../ThreadList/styling/ThreadListItem.scss | 7 +- .../Threads/ThreadList/styling/index.scss | 1 + src/i18n/de.json | 2 + src/i18n/en.json | 2 + src/i18n/es.json | 2 + src/i18n/fr.json | 2 + src/i18n/hi.json | 2 + src/i18n/it.json | 2 + src/i18n/ja.json | 2 + src/i18n/ko.json | 2 + src/i18n/nl.json | 2 + src/i18n/pt.json | 2 + src/i18n/ru.json | 6 +- src/i18n/tr.json | 2 + src/styling/_global-theme-variables.scss | 5 ++ 44 files changed, 630 insertions(+), 80 deletions(-) create mode 100644 src/components/Button/ToggleSidebarButton.tsx create mode 100644 src/components/ChannelHeader/hooks/useIsMobileViewport.ts create mode 100644 src/components/ChannelList/ChannelListHeader.tsx create mode 100644 src/components/ChannelList/styling/ChannelListHeader.scss create mode 100644 src/components/Notifications/notificationOrigin.ts create mode 100644 src/components/ResizableContainer/styling/ResizableContainer.scss create mode 100644 src/components/Thread/ThreadHeaderMain.tsx create mode 100644 src/components/Thread/styling/ThreadHeaderMain.scss create mode 100644 src/components/Threads/ThreadList/ThreadListHeader.tsx create mode 100644 src/components/Threads/ThreadList/styling/ThreadListHeader.scss diff --git a/examples/vite/src/index.scss b/examples/vite/src/index.scss index 55da70831..7374ef6e7 100644 --- a/examples/vite/src/index.scss +++ b/examples/vite/src/index.scss @@ -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 { @@ -89,14 +92,6 @@ 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 { .str-chat__dropzone-root--thread, .str-chat__thread-container { @@ -106,25 +101,45 @@ body { } } - @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; + @media (min-width: 768px) { + .str-chat__dropzone-root--thread, + .str-chat__thread-container { + //flex: 0 0 360px; + width: 100%; + 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__thread-list-container.str-chat__thread-list-container--open { + width: 100%; + max-width: 360px; + } + } + + /* Tablet/narrow (768px–1100px): when thread open, collapse channel so thread has space. */ + @media (min-width: 768px) and (max-width: 1100px) { + .str-chat__container: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; + max-width: none; + } } } @container (max-width: 860px) { - .str-chat__channel-list, - .str-chat__chat-view__selector { - display: none; + .str-chat__thread-container { + width: 100%; + max-width: initial; } } } diff --git a/src/components/Button/ToggleSidebarButton.tsx b/src/components/Button/ToggleSidebarButton.tsx new file mode 100644 index 000000000..e14d3904d --- /dev/null +++ b/src/components/Button/ToggleSidebarButton.tsx @@ -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 ? ( + */} + + +
{displayTitle}
{onlineStatusText != null && ( diff --git a/src/components/ChannelHeader/hooks/useIsMobileViewport.ts b/src/components/ChannelHeader/hooks/useIsMobileViewport.ts new file mode 100644 index 000000000..1ad58ce3c --- /dev/null +++ b/src/components/ChannelHeader/hooks/useIsMobileViewport.ts @@ -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; +}; diff --git a/src/components/ChannelHeader/styling/ChannelHeader.scss b/src/components/ChannelHeader/styling/ChannelHeader.scss index 217e2c0b9..73666a651 100644 --- a/src/components/ChannelHeader/styling/ChannelHeader.scss +++ b/src/components/ChannelHeader/styling/ChannelHeader.scss @@ -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; @@ -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; diff --git a/src/components/ChannelList/ChannelList.tsx b/src/components/ChannelList/ChannelList.tsx index a1aebbcc5..3c355c492 100644 --- a/src/components/ChannelList/ChannelList.tsx +++ b/src/components/ChannelList/ChannelList.tsx @@ -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 = {}; @@ -210,7 +211,7 @@ const UnMemoizedChannelList = (props: ChannelListProps) => { client, closeMobileNav, customClasses, - navOpen = false, + navOpen = true, searchController, setActiveChannel, theme, @@ -382,6 +383,7 @@ const UnMemoizedChannelList = (props: ChannelListProps) => { value={{ channels, hasNextPage, loadNextPage, setChannels }} >
+ {showChannelSearch && (Search ? ( { + const { t } = useTranslationContext(); + const { channel, navOpen } = useChatContext(); + return ( +
+
{t('Chats')}
+ + + +
+ ); +}; diff --git a/src/components/ChannelList/hooks/useMobileNavigation.ts b/src/components/ChannelList/hooks/useMobileNavigation.ts index 0c0b15543..cfec09c3e 100644 --- a/src/components/ChannelList/hooks/useMobileNavigation.ts +++ b/src/components/ChannelList/hooks/useMobileNavigation.ts @@ -1,5 +1,7 @@ import { useEffect } from 'react'; +const MOBILE_NAV_BREAKPOINT = 768; + export const useMobileNavigation = ( channelListRef: React.RefObject, navOpen: boolean, @@ -7,6 +9,9 @@ export const useMobileNavigation = ( ) => { useEffect(() => { const handleClickOutside = (event: MouseEvent) => { + if (typeof window !== 'undefined' && window.innerWidth >= MOBILE_NAV_BREAKPOINT) { + return; + } if ( closeMobileNav && channelListRef.current && diff --git a/src/components/ChannelList/styling/ChannelList.scss b/src/components/ChannelList/styling/ChannelList.scss index a62f3707b..650aaee14 100644 --- a/src/components/ChannelList/styling/ChannelList.scss +++ b/src/components/ChannelList/styling/ChannelList.scss @@ -139,4 +139,32 @@ @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; + inset-inline-start: var(--str-chat__chat-view__selector-width, 0); + 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; + } + } } diff --git a/src/components/ChannelList/styling/ChannelListHeader.scss b/src/components/ChannelList/styling/ChannelListHeader.scss new file mode 100644 index 000000000..34084e3eb --- /dev/null +++ b/src/components/ChannelList/styling/ChannelListHeader.scss @@ -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; + } +} diff --git a/src/components/ChannelList/styling/index.scss b/src/components/ChannelList/styling/index.scss index 89bd774b3..9ef1240c5 100644 --- a/src/components/ChannelList/styling/index.scss +++ b/src/components/ChannelList/styling/index.scss @@ -1 +1,2 @@ @use 'ChannelList'; +@use 'ChannelListHeader'; diff --git a/src/components/Chat/Chat.tsx b/src/components/Chat/Chat.tsx index 3f42b32d8..b452b6486 100644 --- a/src/components/Chat/Chat.tsx +++ b/src/components/Chat/Chat.tsx @@ -27,8 +27,10 @@ export type ChatProps = { defaultLanguage?: SupportedTranslations; /** Instance of Stream i18n */ i18nInstance?: Streami18n; - /** Initial status of mobile navigation */ + /** Initial status of mobile navigation. Ignored when initialNavOpenResponsive is true. */ initialNavOpen?: boolean; + /** When true, sidebar (ChannelList/ThreadList + selector) is open on load; it closes when a channel or thread is selected. */ + initialNavOpenResponsive?: boolean; /** Instance of SearchController class that allows to control all the search operations. */ searchController?: SearchController; /** Used for injecting className/s to the Channel and ChannelList components */ @@ -55,6 +57,7 @@ export const Chat = (props: PropsWithChildren) => { defaultLanguage, i18nInstance, initialNavOpen = true, + initialNavOpenResponsive = true, isMessageAIGenerated, searchController: customChannelSearchController, theme = 'messaging light', @@ -71,7 +74,13 @@ export const Chat = (props: PropsWithChildren) => { openMobileNav, setActiveChannel, translators, - } = useChat({ client, defaultLanguage, i18nInstance, initialNavOpen }); + } = useChat({ + client, + defaultLanguage, + i18nInstance, + initialNavOpen, + initialNavOpenResponsive, + }); const channelsQueryState = useChannelsQueryState(); diff --git a/src/components/Chat/hooks/useChat.ts b/src/components/Chat/hooks/useChat.ts index 92497101f..c4cd406e8 100644 --- a/src/components/Chat/hooks/useChat.ts +++ b/src/components/Chat/hooks/useChat.ts @@ -18,11 +18,20 @@ import type { StreamChat, } from 'stream-chat'; +/** Viewport width (px) above which the sidebar is open by default when using responsive initial nav state. */ +export const NAV_SIDEBAR_DESKTOP_BREAKPOINT = 768; + +/** With responsive nav: sidebar is open on load (so ChannelList/ThreadList + selector visible); close on channel/thread selection. */ +const getDefaultNavOpenFromViewport = (): boolean => true; + export type UseChatParams = { client: StreamChat; defaultLanguage?: SupportedTranslations; i18nInstance?: Streami18n; + /** Initial open state of the sidebar. Ignored when initialNavOpenResponsive is true. */ initialNavOpen?: boolean; + /** When true, initial nav state is open so sidebar (ChannelList/ThreadList + selector) is visible; close on channel/thread selection. */ + initialNavOpenResponsive?: boolean; }; export const useChat = ({ @@ -30,6 +39,7 @@ export const useChat = ({ defaultLanguage = 'en', i18nInstance, initialNavOpen, + initialNavOpenResponsive = false, }: UseChatParams) => { const [translators, setTranslators] = useState({ t: defaultTranslatorFunction, @@ -39,7 +49,10 @@ export const useChat = ({ const [channel, setChannel] = useState(); const [mutes, setMutes] = useState>([]); - const [navOpen, setNavOpen] = useState(initialNavOpen); + const [navOpen, setNavOpen] = useState(() => { + if (initialNavOpenResponsive) return getDefaultNavOpenFromViewport() ?? true; + return initialNavOpen === false ? false : true; + }); const [latestMessageDatesByChannels, setLatestMessageDatesByChannels] = useState({}); const clientMutes = (client.user as OwnUserResponse)?.mutes ?? []; @@ -132,7 +145,10 @@ export const useChat = ({ } setChannel(activeChannel); - closeMobileNav(); + const isMobileViewport = + typeof window !== 'undefined' && + window.innerWidth < NAV_SIDEBAR_DESKTOP_BREAKPOINT; + if (isMobileViewport) closeMobileNav(); }, [], ); diff --git a/src/components/ChatView/ChatView.tsx b/src/components/ChatView/ChatView.tsx index e3c475cc3..1ab1e499e 100644 --- a/src/components/ChatView/ChatView.tsx +++ b/src/components/ChatView/ChatView.tsx @@ -212,6 +212,7 @@ const unreadThreadCountSelector = ({ unreadThreadCount }: ThreadManagerState) => export const ChatViewChannelsSelectorButton = () => { const { activeChatView, setActiveChatView } = useChatViewContext(); + const { openMobileNav } = useChatContext('ChatViewChannelsSelectorButton'); const { t } = useTranslationContext(); return ( @@ -220,14 +221,17 @@ export const ChatViewChannelsSelectorButton = () => { aria-selected={activeChatView === 'channels'} Icon={IconBubble3ChatMessage} isActive={activeChatView === 'channels'} - onPointerDown={() => setActiveChatView('channels')} + onPointerDown={() => { + openMobileNav(); + setActiveChatView('channels'); + }} text={t('Channels')} /> ); }; export const ChatViewThreadsSelectorButton = () => { - const { client } = useChatContext(); + const { client, openMobileNav } = useChatContext(); const { unreadThreadCount } = useStateStore( client.threads.state, unreadThreadCountSelector, @@ -240,7 +244,10 @@ export const ChatViewThreadsSelectorButton = () => { return ( setActiveChatView('threads')} + onPointerDown={() => { + openMobileNav(); + setActiveChatView('threads'); + }} > @@ -274,13 +281,21 @@ export const defaultChatViewSelectorItemSet: ChatViewSelectorEntry[] = [ const ChatViewSelector = ({ itemSet = defaultChatViewSelectorItemSet, -}: ChatViewSelectorProps) => ( -
- {itemSet.map(({ Component, type }) => ( - - ))} -
-); +}: ChatViewSelectorProps) => { + const { navOpen } = useChatContext('ChatView.Selector'); + return ( +
+ {itemSet.map(({ Component, type }) => ( + + ))} +
+ ); +}; ChatView.Channels = ChannelsView; ChatView.Threads = ThreadsView; diff --git a/src/components/ChatView/styling/ChatView.scss b/src/components/ChatView/styling/ChatView.scss index 3cfd31ef9..97686563e 100644 --- a/src/components/ChatView/styling/ChatView.scss +++ b/src/components/ChatView/styling/ChatView.scss @@ -1,6 +1,8 @@ .str-chat { + /** Width reserved for ChatView.Selector so ChannelList overlay (mobile) can start after it. */ + --str-chat__chat-view__selector-width: 94px; --str-chat-selector-background-color: var(--str-chat__secondary-background-color); - --str-chat-selector-border-color: var(--str-chat__surface-color); + --str-chat-selector-border-color: var(--border-core-subtle); --str-chat-selector-button-color-default: var(--str-chat__text-low-emphasis-color); --str-chat-selector-button-color-selected: var(--str-chat__text-color); @@ -22,6 +24,30 @@ border-right: 1px solid var(--str-chat-selector-border-color); background-color: var(--str-chat-selector-background-color); + /* Mobile: hide when nav closed, show when nav open. */ + @media (max-width: 767px) { + &.str-chat__chat-view__selector--nav-closed { + display: none; + } + + &.str-chat__chat-view__selector--nav-open { + display: flex; + } + } + + /* Desktop (≥768px): collapse when nav closed so main content uses space. */ + @media (min-width: 768px) { + &.str-chat__chat-view__selector--nav-closed { + width: 0; + min-width: 0; + overflow: hidden; + padding-inline: 0; + padding-block: 0; + gap: 0; + border-inline-end: none; + } + } + .str-chat__chat-view__selector-button { --str-chat-icon-height: 20px; --str-chat-icon-width: 20px; diff --git a/src/components/MessageInput/styling/MessageComposer.scss b/src/components/MessageInput/styling/MessageComposer.scss index 65a313b5a..8c482e1d4 100644 --- a/src/components/MessageInput/styling/MessageComposer.scss +++ b/src/components/MessageInput/styling/MessageComposer.scss @@ -39,6 +39,7 @@ .str-chat__message-composer-container { width: 100%; + min-height: fit-content; display: flex; flex-direction: column; align-items: center; @@ -83,7 +84,9 @@ align-items: end; width: 100%; gap: var(--spacing-xs); - padding: calc(var(--spacing-sm) - 1px); // compensate for the 1px border of the composer container + padding: calc( + var(--spacing-sm) - 1px + ); // compensate for the 1px border of the composer container $controls-containers-min-height: 26px; diff --git a/src/components/MessageInput/styling/SendToChannelCheckbox.scss b/src/components/MessageInput/styling/SendToChannelCheckbox.scss index ec545cf42..fe3bf3275 100644 --- a/src/components/MessageInput/styling/SendToChannelCheckbox.scss +++ b/src/components/MessageInput/styling/SendToChannelCheckbox.scss @@ -36,16 +36,19 @@ .str-chat__send-to-channel-checkbox__visual { width: 20px; height: 20px; - border: 1px solid var(--control-checkbox-border, #D5DBE1); + border: 1px solid var(--control-checkbox-border, #d5dbe1); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center; background: transparent; - transition: background-color 0.15s ease, border-color 0.15s ease; + transition: + background-color 0.15s ease, + border-color 0.15s ease; } - .str-chat__send-to-channel-checkbox__input:checked + .str-chat__send-to-channel-checkbox__visual { + .str-chat__send-to-channel-checkbox__input:checked + + .str-chat__send-to-channel-checkbox__visual { background-color: var(--control-radiocheck-bg-selected, var(--accent-primary)); border-color: var(--control-radiocheck-bg-selected, var(--accent-primary)); } @@ -64,14 +67,17 @@ } } - .str-chat__send-to-channel-checkbox__input:checked + .str-chat__send-to-channel-checkbox__visual .str-chat__send-to-channel-checkbox__checkmark { + .str-chat__send-to-channel-checkbox__input:checked + + .str-chat__send-to-channel-checkbox__visual + .str-chat__send-to-channel-checkbox__checkmark { opacity: 1; } .str-chat__send-to-channel-checkbox__label { font: var(--str-chat__metadata-default-text); - transition: color 0.15s ease, border-color 0.15s ease; + transition: + color 0.15s ease, + border-color 0.15s ease; } - } -} \ No newline at end of file +} diff --git a/src/components/Notifications/notificationOrigin.ts b/src/components/Notifications/notificationOrigin.ts new file mode 100644 index 000000000..beeb652ca --- /dev/null +++ b/src/components/Notifications/notificationOrigin.ts @@ -0,0 +1,5 @@ +/** + * Panel where the notification was registered (channel vs thread). + * Use in origin.context.panel when publishing so NotificationList can filter by panel. + */ +export type NotificationOriginPanel = 'channel' | 'thread'; diff --git a/src/components/ResizableContainer/styling/ResizableContainer.scss b/src/components/ResizableContainer/styling/ResizableContainer.scss new file mode 100644 index 000000000..b4eec3d47 --- /dev/null +++ b/src/components/ResizableContainer/styling/ResizableContainer.scss @@ -0,0 +1,81 @@ +.str-chat__resize-container { + // layout only; panels and handles are children + + .str-chat__resize-container__center { + display: flex; + flex-direction: column; + } +} + +.str-chat__resize-panel { + .str-chat__resize-panel__content { + display: flex; + flex-direction: column; + } + + .str-chat__resize-panel__handle { + position: relative; + z-index: 1; + background: transparent; + transition: background-color 0.15s ease; + + &:hover, + &:focus-visible { + background: var(--str-chat__primary-color-10, rgba(0 0 0 / 0.06)); + } + + &:active { + background: var(--str-chat__primary-color-15, rgba(0 0 0 / 0.09)); + } + } + + .str-chat__resize-panel__handle-line { + display: flex; + align-items: center; + justify-content: center; + width: 1px; + height: 100%; + min-height: 24px; + background: var(--str-chat__border-color, rgba(0 0 0 / 0.12)); + } + + .str-chat__resize-panel__handle-icon { + width: 16px; + height: 16px; + color: var(--str-chat__secondary-color, rgba(0 0 0 / 0.5)); + pointer-events: none; + } + + .str-chat__resize-panel__expand-tab { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + min-width: 20px; + height: 100%; + min-height: 48px; + padding: 0; + border: none; + background: transparent; + color: var(--str-chat__secondary-color, rgba(0 0 0 / 0.5)); + cursor: col-resize; + transition: + background-color 0.15s ease, + color 0.15s ease; + + &:hover { + background: var(--str-chat__primary-color-10, rgba(0 0 0 / 0.06)); + color: var(--str-chat__primary-color, #006cff); + } + + &:focus-visible { + outline: 2px solid var(--str-chat__primary-color, #006cff); + outline-offset: 2px; + } + + svg { + width: 16px; + height: 16px; + } + } +} diff --git a/src/components/Thread/Thread.tsx b/src/components/Thread/Thread.tsx index 93bcad276..5ff6e9338 100644 --- a/src/components/Thread/Thread.tsx +++ b/src/components/Thread/Thread.tsx @@ -8,6 +8,7 @@ import { MessageInput, MessageInputFlat } from '../MessageInput'; import type { MessageListProps, VirtualizedMessageListProps } from '../MessageList'; import { MessageList, VirtualizedMessageList } from '../MessageList'; import { ThreadHeader as DefaultThreadHeader } from './ThreadHeader'; +import { ThreadHeaderMain as DefaultThreadHeaderMain } from './ThreadHeaderMain'; import { ThreadHead as DefaultThreadHead } from '../Thread/ThreadHead'; import { @@ -22,6 +23,7 @@ import { useStateStore } from '../../store'; import type { MessageProps, MessageUIComponentProps } from '../Message/types'; import type { MessageActionsArray } from '../Message/utils'; import type { ThreadState } from 'stream-chat'; +import { useChatViewContext } from '../ChatView'; export type ThreadProps = { /** Additional props for `MessageInput` component: [available props](https://getstream.io/chat/docs/sdk/react/message-input-components/message_input/#props) */ @@ -86,7 +88,7 @@ const ThreadInner = (props: ThreadProps & { key: string }) => { messageActions = Object.keys(MESSAGE_ACTIONS), virtualized, } = props; - + const { activeChatView } = useChatViewContext(); const threadInstance = useThreadContext(); const { @@ -178,7 +180,12 @@ const ThreadInner = (props: ThreadProps & { key: string }) => { }} >
- + {activeChatView === 'threads' ? ( + // todo: add ThreadHeaderMain alongisde ThreadHeader property to ComponentContext? + + ) : ( + + )} ({ replyCount }); + +export type ThreadHeaderMainProps = { + /** UI component to display menu icon, defaults to IconLayoutAlignLeft*/ + MenuIcon?: React.ComponentType; + /** Set title manually */ + title?: string; +}; + +/** + * This header is the default header rendered for Thread in 'threads' chat view. + * It provides layout control capabilities - toggling sidebar open / close. + * The purpose is to provide layout control for the main message list in threads view. + */ +export const ThreadHeaderMain = ({ + MenuIcon = IconLayoutAlignLeft, + title, +}: ThreadHeaderMainProps) => { + const { t } = useTranslationContext('ThreadHeader'); + const thread = useThreadContext(); + + const { replyCount } = useStateStore(thread?.state, threadStateSelector) ?? { + replyCount: 0, + }; + + return ( +
+ + + +
+
{title ?? t('Thread')}
+
+ {t('replyCount', { count: replyCount })} +
+
+
+ ); +}; diff --git a/src/components/Thread/styling/ThreadHeaderMain.scss b/src/components/Thread/styling/ThreadHeaderMain.scss new file mode 100644 index 000000000..e8fce37bd --- /dev/null +++ b/src/components/Thread/styling/ThreadHeaderMain.scss @@ -0,0 +1,25 @@ +.str-chat__thread-header--main { + display: flex; + align-items: center; + gap: var(--spacing-xs); + padding: var(--spacing-md); + border-bottom: 1px solid var(--border-core-default, #d5dbe1); + background: var(--background-elevation-elevation-1, #fff); + + .str-chat__thread-header--main__details { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + + .str-chat__thread-header--main__title { + font: var(--str-chat__heading-sm-text); + color: var(--text-primary); + } + + .str-chat__thread-header--main__subtitle { + font: var(--str-chat__caption-default-text); + color: var(--text-secondary); + } + } +} diff --git a/src/components/Thread/styling/index.scss b/src/components/Thread/styling/index.scss index 99a8b1908..7cbe56f54 100644 --- a/src/components/Thread/styling/index.scss +++ b/src/components/Thread/styling/index.scss @@ -1 +1,2 @@ @use 'Thread'; +@use 'ThreadHeaderMain'; diff --git a/src/components/Threads/ThreadList/ThreadList.tsx b/src/components/Threads/ThreadList/ThreadList.tsx index 6820fa560..422d11754 100644 --- a/src/components/Threads/ThreadList/ThreadList.tsx +++ b/src/components/Threads/ThreadList/ThreadList.tsx @@ -1,6 +1,7 @@ import React, { useEffect } from 'react'; import type { ComputeItemKey, VirtuosoProps } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso'; +import clsx from 'clsx'; import type { Thread, ThreadManagerState } from 'stream-chat'; @@ -10,6 +11,7 @@ import { ThreadListUnseenThreadsBanner as DefaultThreadListUnseenThreadsBanner } import { ThreadListLoadingIndicator as DefaultThreadListLoadingIndicator } from './ThreadListLoadingIndicator'; import { useChatContext, useComponentContext } from '../../../context'; import { useStateStore } from '../../../store'; +import { ThreadListHeader } from './ThreadListHeader'; const selector = (nextValue: ThreadManagerState) => ({ threads: nextValue.threads }); @@ -43,7 +45,7 @@ export const useThreadList = () => { }; export const ThreadList = ({ virtuosoProps }: ThreadListProps) => { - const { client } = useChatContext(); + const { client, navOpen = true } = useChatContext(); const { ThreadListEmptyPlaceholder = DefaultThreadListEmptyPlaceholder, ThreadListItem = DefaultThreadListItem, @@ -55,7 +57,12 @@ export const ThreadList = ({ virtuosoProps }: ThreadListProps) => { useThreadList(); return ( -
+
+ {/* TODO: allow re-load on stale ThreadManager state */} { + const { t } = useTranslationContext(); + const { navOpen } = useChatContext(); + const { activeThread } = useThreadsViewContext(); + return ( +
+
{t('Threads')}
+ + + +
+ ); +}; diff --git a/src/components/Threads/ThreadList/ThreadListItemUI.tsx b/src/components/Threads/ThreadList/ThreadListItemUI.tsx index 72047ee40..b7e80c609 100644 --- a/src/components/Threads/ThreadList/ThreadListItemUI.tsx +++ b/src/components/Threads/ThreadList/ThreadListItemUI.tsx @@ -26,6 +26,7 @@ import { IconVideo, } from '../../Icons'; import clsx from 'clsx'; +import { NAV_SIDEBAR_DESKTOP_BREAKPOINT } from '../../Chat'; export type ThreadListItemUIProps = ComponentPropsWithoutRef<'button'>; @@ -81,6 +82,7 @@ export const ThreadListItemUI = (props: ThreadListItemUIProps) => { const { displayTitle: channelDisplayTitle } = useChannelPreviewInfo({ channel }); const { t } = useTranslationContext('ThreadListItemUI'); + const { closeMobileNav } = useChatContext('ThreadListItemUI'); const { activeThread, setActiveThread } = useThreadsViewContext(); const avatarProps: Partial | undefined = deletedAt @@ -107,7 +109,15 @@ export const ThreadListItemUI = (props: ThreadListItemUIProps) => { aria-pressed={activeThread === thread} className='str-chat__thread-list-item' data-thread-id={thread.id} - onClick={() => setActiveThread(thread)} + onClick={() => { + if ( + typeof window !== 'undefined' && + window.innerWidth < NAV_SIDEBAR_DESKTOP_BREAKPOINT + ) { + closeMobileNav(); + } + setActiveThread(thread); + }} role='option' {...props} > @@ -128,7 +138,9 @@ export const ThreadListItemUI = (props: ThreadListItemUIProps) => { )} {ContentTypeIcon && } - {text} + + {text} +
diff --git a/src/components/Threads/ThreadList/styling/ThreadList.scss b/src/components/Threads/ThreadList/styling/ThreadList.scss index 980fcf660..9f4a82919 100644 --- a/src/components/Threads/ThreadList/styling/ThreadList.scss +++ b/src/components/Threads/ThreadList/styling/ThreadList.scss @@ -16,6 +16,34 @@ display: flex; flex-direction: column; height: 100%; + + /* Mobile: hide when nav closed; when open show as overlay. */ + @media (max-width: 767px) { + display: none; + + &.str-chat__thread-list-container--open { + display: flex; + position: absolute; + inset-inline-start: var(--str-chat__chat-view__selector-width, 0); + top: 0; + bottom: 0; + z-index: 1; + min-width: 280px; + max-width: 100%; + box-shadow: var(--str-chat__thread-list-box-shadow); + } + } + + /* Desktop (≥768px): collapse when nav closed so main content uses space. */ + @media (min-width: 768px) { + &:not(.str-chat__thread-list-container--open) { + flex: 0 0 0; + width: 0; + min-width: 0; + max-width: 0; + overflow: hidden; + } + } } .str-chat__thread-list { diff --git a/src/components/Threads/ThreadList/styling/ThreadListHeader.scss b/src/components/Threads/ThreadList/styling/ThreadListHeader.scss new file mode 100644 index 000000000..cbbb6b5af --- /dev/null +++ b/src/components/Threads/ThreadList/styling/ThreadListHeader.scss @@ -0,0 +1,26 @@ +.str-chat__thread-list__header { + display: flex; + align-items: center; + padding: var(--spacing-md); + height: var(--str-chat__channel-header-height); + width: 100%; + + .str-chat__thread-list__header__title { + flex: 1; + font: var(--str-chat__heading-lg-text); + color: var(--text-primary); + } + + &.str-chat__thread-list__header--sidebar-collapsed { + flex: 0 0 0; + width: 0; + min-width: 0; + max-width: 0; + overflow: hidden; + padding: 0; + + .str-chat__header-sidebar-toggle { + // Compact styling when sidebar collapsed + } + } +} diff --git a/src/components/Threads/ThreadList/styling/ThreadListItem.scss b/src/components/Threads/ThreadList/styling/ThreadListItem.scss index aab4de1ed..f3aea9e80 100644 --- a/src/components/Threads/ThreadList/styling/ThreadListItem.scss +++ b/src/components/Threads/ThreadList/styling/ThreadListItem.scss @@ -1,6 +1,9 @@ +@use '../../../../styling/utils'; + .str-chat__thread-list-item-container { border-bottom: 1px solid var(--border-core-subtle); padding: var(--spacing-xxs); + max-width: 100%; } .str-chat__thread-list-item { @@ -13,10 +16,10 @@ border: none; cursor: pointer; text-align: start; - font-family: var(--typography-font-family-sans); background: var(--background-elevation-elevation-1); border-radius: var(--radius-lg); width: 100%; + max-width: 100%; background: var(--background-elevation-elevation-1); &:not(:disabled):hover { @@ -97,6 +100,8 @@ line-height: var(--typography-line-height-normal); color: var(--text-tertiary); white-space: nowrap; + min-width: 0; + @include utils.ellipsis-text; } .str-chat__thread-list-item__message-preview { diff --git a/src/components/Threads/ThreadList/styling/index.scss b/src/components/Threads/ThreadList/styling/index.scss index ab312588f..063a79177 100644 --- a/src/components/Threads/ThreadList/styling/index.scss +++ b/src/components/Threads/ThreadList/styling/index.scss @@ -1,2 +1,3 @@ @use 'ThreadList'; +@use 'ThreadListHeader'; @use 'ThreadListItem'; diff --git a/src/i18n/de.json b/src/i18n/de.json index 34ecc4f9b..c4ac3fe3e 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -54,6 +54,7 @@ "aria/Channel list": "Kanalliste", "aria/Channel search results": "Kanalsuchergebnisse", "aria/Close thread": "Thread schließen", + "aria/Collapse sidebar": "Seitenleiste einklappen", "aria/Copy Message Text": "Nachrichtentext kopieren", "aria/Delete Message": "Nachricht löschen", "aria/Download attachment": "Anhang herunterladen", @@ -107,6 +108,7 @@ "Cannot seek in the recording": "In der Aufnahme kann nicht gesucht werden", "Channel Missing": "Kanal fehlt", "Channels": "Kanäle", + "Chats": "Chats", "Choose between 2 to 10 options": "Wähle zwischen 2 und 10 Optionen", "Close": "Schließen", "Close emoji picker": "Emoji-Auswahl schließen", diff --git a/src/i18n/en.json b/src/i18n/en.json index 6dba06dc7..e1b7f7d5f 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -54,6 +54,7 @@ "aria/Channel list": "Channel list", "aria/Channel search results": "Channel search results", "aria/Close thread": "Close thread", + "aria/Collapse sidebar": "Collapse sidebar", "aria/Copy Message Text": "Copy Message Text", "aria/Delete Message": "Delete Message", "aria/Download attachment": "Download attachment", @@ -107,6 +108,7 @@ "Cannot seek in the recording": "Cannot seek in the recording", "Channel Missing": "Channel Missing", "Channels": "Channels", + "Chats": "Chats", "Choose between 2 to 10 options": "Choose between 2 to 10 options", "Close": "Close", "Close emoji picker": "Close emoji picker", diff --git a/src/i18n/es.json b/src/i18n/es.json index a3375dcc3..335e2b5fe 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -61,6 +61,7 @@ "aria/Channel list": "Lista de canales", "aria/Channel search results": "Resultados de búsqueda de canales", "aria/Close thread": "Cerrar hilo", + "aria/Collapse sidebar": "Contraer barra lateral", "aria/Copy Message Text": "Copiar texto del mensaje", "aria/Delete Message": "Eliminar mensaje", "aria/Download attachment": "Descargar adjunto", @@ -114,6 +115,7 @@ "Cannot seek in the recording": "No se puede buscar en la grabación", "Channel Missing": "Falta canal", "Channels": "Canales", + "Chats": "Chats", "Choose between 2 to 10 options": "Elige entre 2 y 10 opciones", "Close": "Cerrar", "Close emoji picker": "Cerrar el selector de emojis", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index e3c37dacc..ddc2b5744 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -61,6 +61,7 @@ "aria/Channel list": "Liste des canaux", "aria/Channel search results": "Résultats de recherche de canaux", "aria/Close thread": "Fermer le fil", + "aria/Collapse sidebar": "Réduire la barre latérale", "aria/Copy Message Text": "Copier le texte du message", "aria/Delete Message": "Supprimer le message", "aria/Download attachment": "Télécharger la pièce jointe", @@ -114,6 +115,7 @@ "Cannot seek in the recording": "Impossible de rechercher dans l'enregistrement", "Channel Missing": "Canal Manquant", "Channels": "Canaux", + "Chats": "Discussions", "Choose between 2 to 10 options": "Choisir entre 2 et 10 options", "Close": "Fermer", "Close emoji picker": "Fermer le sélecteur d'émojis", diff --git a/src/i18n/hi.json b/src/i18n/hi.json index ebc66cb80..620ed409c 100644 --- a/src/i18n/hi.json +++ b/src/i18n/hi.json @@ -54,6 +54,7 @@ "aria/Channel list": "चैनल सूची", "aria/Channel search results": "चैनल खोज परिणाम", "aria/Close thread": "थ्रेड बंद करें", + "aria/Collapse sidebar": "साइडबार संक्षिप्त करें", "aria/Copy Message Text": "संदेश की टेक्स्ट कॉपी करें", "aria/Delete Message": "संदेश डिलीट करें", "aria/Download attachment": "अनुलग्नक डाउनलोड करें", @@ -107,6 +108,7 @@ "Cannot seek in the recording": "रेकॉर्डिंग में खोज नहीं की जा सकती", "Channel Missing": "चैनल उपलब्ध नहीं है", "Channels": "चैनल", + "Chats": "चैट", "Choose between 2 to 10 options": "2 से 10 विकल्प चुनें", "Close": "बंद करे", "Close emoji picker": "इमोजी पिकर बंद करें", diff --git a/src/i18n/it.json b/src/i18n/it.json index 316daa4cf..e05576ec0 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -61,6 +61,7 @@ "aria/Channel list": "Elenco dei canali", "aria/Channel search results": "Risultati della ricerca dei canali", "aria/Close thread": "Chiudi discussione", + "aria/Collapse sidebar": "Comprimi barra laterale", "aria/Copy Message Text": "Copia testo messaggio", "aria/Delete Message": "Elimina messaggio", "aria/Download attachment": "Scarica l'allegato", @@ -114,6 +115,7 @@ "Cannot seek in the recording": "Impossibile cercare nella registrazione", "Channel Missing": "Il canale non esiste", "Channels": "Canali", + "Chats": "Chat", "Choose between 2 to 10 options": "Scegli tra 2 e 10 opzioni", "Close": "Chiudi", "Close emoji picker": "Chiudi il selettore di emoji", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index d1f97cef6..a3f293147 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -53,6 +53,7 @@ "aria/Channel list": "チャンネル一覧", "aria/Channel search results": "チャンネル検索結果", "aria/Close thread": "スレッドを閉じる", + "aria/Collapse sidebar": "サイドバーを折りたたむ", "aria/Copy Message Text": "メッセージテキストをコピー", "aria/Delete Message": "メッセージを削除", "aria/Download attachment": "添付ファイルをダウンロード", @@ -106,6 +107,7 @@ "Cannot seek in the recording": "録音中にシークできません", "Channel Missing": "チャネルがありません", "Channels": "チャンネル", + "Chats": "チャット", "Choose between 2 to 10 options": "2〜10の選択肢から選ぶ", "Close": "閉める", "Close emoji picker": "絵文字ピッカーを閉める", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index 9294c4ba2..fc8ea4eb7 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -53,6 +53,7 @@ "aria/Channel list": "채널 목록", "aria/Channel search results": "채널 검색 결과", "aria/Close thread": "스레드 닫기", + "aria/Collapse sidebar": "사이드바 접기", "aria/Copy Message Text": "메시지 텍스트 복사", "aria/Delete Message": "메시지 삭제", "aria/Download attachment": "첨부 파일 다운로드", @@ -106,6 +107,7 @@ "Cannot seek in the recording": "녹음에서 찾을 수 없습니다", "Channel Missing": "채널 누락", "Channels": "채널", + "Chats": "채팅", "Choose between 2 to 10 options": "2~10개의 선택지 중에서 선택", "Close": "닫기", "Close emoji picker": "이모티콘 선택기 닫기", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index e9ad1ac54..2e3d20611 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -54,6 +54,7 @@ "aria/Channel list": "Kanaallijst", "aria/Channel search results": "Zoekresultaten voor kanalen", "aria/Close thread": "Draad sluiten", + "aria/Collapse sidebar": "Zijbalk samenklappen", "aria/Copy Message Text": "Berichttekst kopiëren", "aria/Delete Message": "Bericht verwijderen", "aria/Download attachment": "Bijlage downloaden", @@ -107,6 +108,7 @@ "Cannot seek in the recording": "Kan niet zoeken in de opname", "Channel Missing": "Kanaal niet gevonden", "Channels": "Kanalen", + "Chats": "Chats", "Choose between 2 to 10 options": "Kies tussen 2 en 10 opties", "Close": "Sluit", "Close emoji picker": "Sluit de emoji-kiezer", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index 6e3cb2e0e..587a74be9 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -61,6 +61,7 @@ "aria/Channel list": "Lista de canais", "aria/Channel search results": "Resultados de pesquisa de canais", "aria/Close thread": "Fechar tópico", + "aria/Collapse sidebar": "Recolher barra lateral", "aria/Copy Message Text": "Copiar texto da mensagem", "aria/Delete Message": "Excluir mensagem", "aria/Download attachment": "Baixar anexo", @@ -114,6 +115,7 @@ "Cannot seek in the recording": "Não é possível buscar na gravação", "Channel Missing": "Canal ausente", "Channels": "Canais", + "Chats": "Conversas", "Choose between 2 to 10 options": "Escolha entre 2 a 10 opções", "Close": "Fechar", "Close emoji picker": "Fechar seletor de emoji", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 4a4de33dc..23b322730 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -68,6 +68,7 @@ "aria/Channel list": "Список каналов", "aria/Channel search results": "Результаты поиска по каналам", "aria/Close thread": "Закрыть тему", + "aria/Collapse sidebar": "Свернуть боковую панель", "aria/Copy Message Text": "Копировать текст сообщения", "aria/Delete Message": "Удалить сообщение", "aria/Download attachment": "Скачать вложение", @@ -121,6 +122,7 @@ "Cannot seek in the recording": "Невозможно осуществить поиск в записи", "Channel Missing": "Канал не найден", "Channels": "Каналы", + "Chats": "Чаты", "Choose between 2 to 10 options": "Выберите от 2 до 10 вариантов", "Close": "Закрыть", "Close emoji picker": "Закрыть окно выбора смайлов", @@ -180,8 +182,8 @@ "File too large": "Файл слишком большой", "fileCount_four": "{{ count }} файла", "fileCount_one": "{{ count }} файл", - "fileCount_few": "", - "fileCount_many": "", + "fileCount_few": "{{ count }} файла", + "fileCount_many": "{{ count }} файлов", "fileCount_other": "{{ count }} файлов", "fileCount_three": "{{ count }} файла", "fileCount_two": "{{ count }} файла", diff --git a/src/i18n/tr.json b/src/i18n/tr.json index 95907391a..9d9d12e53 100644 --- a/src/i18n/tr.json +++ b/src/i18n/tr.json @@ -54,6 +54,7 @@ "aria/Channel list": "Kanal listesi", "aria/Channel search results": "Kanal arama sonuçları", "aria/Close thread": "Konuyu kapat", + "aria/Collapse sidebar": "Kenar çubuğunu daralt", "aria/Copy Message Text": "Mesaj metnini kopyala", "aria/Delete Message": "Mesajı sil", "aria/Download attachment": "Ek indir", @@ -107,6 +108,7 @@ "Cannot seek in the recording": "Kayıtta arama yapılamıyor", "Channel Missing": "Kanal bulunamıyor", "Channels": "Kanallar", + "Chats": "Sohbetler", "Choose between 2 to 10 options": "2 ile 10 seçenek arasından seçin", "Close": "Kapat", "Close emoji picker": "Emoji seçiciyi kapat", diff --git a/src/styling/_global-theme-variables.scss b/src/styling/_global-theme-variables.scss index 540d1d3b3..3c176cb1b 100644 --- a/src/styling/_global-theme-variables.scss +++ b/src/styling/_global-theme-variables.scss @@ -65,6 +65,11 @@ var(--typography-font-size-md) / var(--typography-line-height-normal) var(--str-chat__font-family); + --str-chat__heading-lg-text: normal var(--typography-font-weight-semi-bold) + var(--typography-font-size-xl) / var(--typography-line-height-relaxed) + var(--str-chat__font-family); + color: var(--text-primary, #1a1b25); + // todo: adapt the old text variables to so that they use the new semantic text variables /* The font used for caption texts */ --str-chat__caption-text: 0.75rem/1.3 var(--str-chat__font-family); From a2e1d5260052a26389540661181cedd43d5b8d87 Mon Sep 17 00:00:00 2001 From: martincupela Date: Fri, 6 Mar 2026 19:35:24 +0100 Subject: [PATCH 3/5] fix: remove unnecessary styling --- src/components/ChannelList/styling/ChannelList.scss | 1 - src/components/ChatView/styling/ChatView.scss | 2 -- src/components/Threads/ThreadList/styling/ThreadList.scss | 1 - 3 files changed, 4 deletions(-) diff --git a/src/components/ChannelList/styling/ChannelList.scss b/src/components/ChannelList/styling/ChannelList.scss index 650aaee14..ece55962a 100644 --- a/src/components/ChannelList/styling/ChannelList.scss +++ b/src/components/ChannelList/styling/ChannelList.scss @@ -147,7 +147,6 @@ &.str-chat__channel-list--open { display: flex; position: absolute; - inset-inline-start: var(--str-chat__chat-view__selector-width, 0); top: 0; bottom: 0; z-index: 1; diff --git a/src/components/ChatView/styling/ChatView.scss b/src/components/ChatView/styling/ChatView.scss index 6c185b830..c04d6f310 100644 --- a/src/components/ChatView/styling/ChatView.scss +++ b/src/components/ChatView/styling/ChatView.scss @@ -1,6 +1,4 @@ .str-chat { - /** Width reserved for ChatView.Selector so ChannelList overlay (mobile) can start after it. */ - --str-chat__chat-view__selector-width: 94px; --str-chat-selector-background-color: var(--str-chat__secondary-background-color); --str-chat-selector-border-color: var(--border-core-subtle); diff --git a/src/components/Threads/ThreadList/styling/ThreadList.scss b/src/components/Threads/ThreadList/styling/ThreadList.scss index 9f4a82919..df6d46370 100644 --- a/src/components/Threads/ThreadList/styling/ThreadList.scss +++ b/src/components/Threads/ThreadList/styling/ThreadList.scss @@ -24,7 +24,6 @@ &.str-chat__thread-list-container--open { display: flex; position: absolute; - inset-inline-start: var(--str-chat__chat-view__selector-width, 0); top: 0; bottom: 0; z-index: 1; From 7349954c0986466a5bdfc7b2a44fba79e44dab1a Mon Sep 17 00:00:00 2001 From: martincupela Date: Fri, 6 Mar 2026 19:42:41 +0100 Subject: [PATCH 4/5] fix(demo): allow to display Thread alongside the Channel on wider resolutions --- examples/vite/src/index.scss | 38 +++++++++++++++--------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/examples/vite/src/index.scss b/examples/vite/src/index.scss index 7374ef6e7..d108d7875 100644 --- a/examples/vite/src/index.scss +++ b/examples/vite/src/index.scss @@ -101,39 +101,33 @@ body { } } + /* Desktop (≥768px): thread fixed width next to channel; channel takes remaining space. */ @media (min-width: 768px) { - .str-chat__dropzone-root--thread, - .str-chat__thread-container { - //flex: 0 0 360px; - width: 100%; - max-width: 360px; - } - - .str-chat__thread-list-container.str-chat__thread-list-container--open { - width: 100%; - max-width: 360px; - } - } - - /* Tablet/narrow (768px–1100px): when thread open, collapse channel so thread has space. */ - @media (min-width: 768px) and (max-width: 1100px) { .str-chat__container: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; + flex: 1 1 auto; min-width: 0; - max-width: 0; - overflow: hidden; } > .str-chat__thread-container, > .str-chat__dropzone-root--thread { - flex: 1 1 auto; - min-width: 0; - max-width: none; + flex: 0 0 360px; + width: 360px; + max-width: 360px; } } + + .str-chat__dropzone-root--thread, + .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) { From 389a70a83659de2ea7838cac5f4f63fb1b1d4424 Mon Sep 17 00:00:00 2001 From: martincupela Date: Fri, 6 Mar 2026 19:56:59 +0100 Subject: [PATCH 5/5] fix(demo): expand thread in threads view to max space --- examples/vite/src/index.scss | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/examples/vite/src/index.scss b/examples/vite/src/index.scss index d108d7875..ff369eee6 100644 --- a/examples/vite/src/index.scss +++ b/examples/vite/src/index.scss @@ -92,18 +92,27 @@ body { //max-width: none; } - .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%; } } - /* Desktop (≥768px): thread fixed width next to channel; channel takes remaining space. */ + .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__container:has(.str-chat__thread-container) { + .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; @@ -118,8 +127,8 @@ body { } } - .str-chat__dropzone-root--thread, - .str-chat__thread-container { + .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; }