{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 (
+
+ );
+};
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..ece55962a 100644
--- a/src/components/ChannelList/styling/ChannelList.scss
+++ b/src/components/ChannelList/styling/ChannelList.scss
@@ -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;
+ }
+ }
}
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 d72e15c58..94c1511a8 100644
--- a/src/components/ChatView/ChatView.tsx
+++ b/src/components/ChatView/ChatView.tsx
@@ -228,6 +228,7 @@ export const ChatViewChannelsSelectorButton = ({
iconOnly = true,
}: ChatViewSelectorItemProps) => {
const { activeChatView, setActiveChatView } = useChatViewContext();
+ const { openMobileNav } = useChatContext('ChatViewChannelsSelectorButton');
const { t } = useTranslationContext();
return (
@@ -237,7 +238,10 @@ export const ChatViewChannelsSelectorButton = ({
Icon={IconBubble3ChatMessage}
iconOnly={iconOnly}
isActive={activeChatView === 'channels'}
- onPointerDown={() => setActiveChatView('channels')}
+ onPointerDown={() => {
+ openMobileNav();
+ setActiveChatView('channels');
+ }}
text={t('Channels')}
/>
);
@@ -246,7 +250,7 @@ export const ChatViewChannelsSelectorButton = ({
export const ChatViewThreadsSelectorButton = ({
iconOnly = true,
}: ChatViewSelectorItemProps) => {
- const { client } = useChatContext();
+ const { client, openMobileNav } = useChatContext();
const { unreadThreadCount } = useStateStore(
client.threads.state,
unreadThreadCountSelector,
@@ -260,7 +264,10 @@ export const ChatViewThreadsSelectorButton = ({
setActiveChatView('threads')}
+ onPointerDown={() => {
+ openMobileNav();
+ setActiveChatView('threads');
+ }}
text={t('Threads')}
>
@@ -296,13 +303,21 @@ export const defaultChatViewSelectorItemSet: ChatViewSelectorEntry[] = [
const ChatViewSelector = ({
iconOnly = true,
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 5c60cd9d1..c04d6f310 100644
--- a/src/components/ChatView/styling/ChatView.scss
+++ b/src/components/ChatView/styling/ChatView.scss
@@ -1,6 +1,6 @@
.str-chat {
--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 +22,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-container {
display: flex;
position: relative;
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 (
+
+ );
+};
diff --git a/src/components/Threads/ThreadList/ThreadListItemUI.tsx b/src/components/Threads/ThreadList/ThreadListItemUI.tsx
index 75473b7f9..f2e9095b9 100644
--- a/src/components/Threads/ThreadList/ThreadListItemUI.tsx
+++ b/src/components/Threads/ThreadList/ThreadListItemUI.tsx
@@ -12,6 +12,7 @@ import { useThreadListItemContext } from './ThreadListItem';
import { useStateStore } from '../../../store';
import { Badge } from '../../Badge';
import { SummarizedMessagePreview } from '../../SummarizedMessagePreview';
+import { NAV_SIDEBAR_DESKTOP_BREAKPOINT } from '../../Chat';
export type ThreadListItemUIProps = ComponentPropsWithoutRef<'button'>;
@@ -47,6 +48,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
@@ -73,7 +75,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}
>
diff --git a/src/components/Threads/ThreadList/styling/ThreadList.scss b/src/components/Threads/ThreadList/styling/ThreadList.scss
index 980fcf660..df6d46370 100644
--- a/src/components/Threads/ThreadList/styling/ThreadList.scss
+++ b/src/components/Threads/ThreadList/styling/ThreadList.scss
@@ -16,6 +16,33 @@
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;
+ 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/ThreadListItemUI.scss b/src/components/Threads/ThreadList/styling/ThreadListItemUI.scss
index dc6b1f7e5..c684008e0 100644
--- a/src/components/Threads/ThreadList/styling/ThreadListItemUI.scss
+++ b/src/components/Threads/ThreadList/styling/ThreadListItemUI.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 {
@@ -106,6 +109,6 @@
line-height: var(--typography-line-height-normal);
color: var(--text-tertiary);
white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+ min-width: 0;
+ @include utils.ellipsis-text;
}
diff --git a/src/components/Threads/ThreadList/styling/index.scss b/src/components/Threads/ThreadList/styling/index.scss
index 17e7b4008..9c1e08d05 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 'ThreadListItemUI';
diff --git a/src/i18n/de.json b/src/i18n/de.json
index 498a4aba1..3bb26268a 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 925dab652..c4572511e 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 01d8c9a9f..cf68f55c1 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 5011207e5..b0535612c 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 58228da14..1c4f16b2e 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 ca1e9ada5..f37d1ff46 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 965e352f5..1da12e4db 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 aad1e6788..2f0bd0fcc 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 41f55471e..c22b16dd6 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 da9fa8e85..20038a7dd 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 fd963839f..3597f6e17 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 aed0805ae..6f32a41b0 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);