diff --git a/examples/vite/src/App.tsx b/examples/vite/src/App.tsx index 35ae6300d5..08b72bb7df 100644 --- a/examples/vite/src/App.tsx +++ b/examples/vite/src/App.tsx @@ -16,8 +16,10 @@ import { import { Attachment, type AttachmentProps, + Button, Chat, ChatView, + createIcon, MessageReactions, type NotificationListProps, NotificationList, @@ -27,6 +29,7 @@ import { type ReactionOptions, mapEmojiMartData, useCreateChatClient, + useTranslationContext, Search, } from 'stream-chat-react'; import { createTextComposerEmojiMiddleware, EmojiPicker } from 'stream-chat-react/emojis'; @@ -37,6 +40,7 @@ import { humanId } from 'human-id'; import { appSettingsStore, useAppSettingsSelector } from './AppSettings'; import { DESKTOP_LAYOUT_BREAKPOINT } from './ChatLayout/constants.ts'; import { ChannelsPanels, ThreadsPanels } from './ChatLayout/Panels.tsx'; +import { SidebarProvider, useSidebar } from './ChatLayout/SidebarContext.tsx'; import { ChatViewSelectorWidthSync, PanelLayoutStyleSync, @@ -162,6 +166,36 @@ const ConfigurableNotificationList = (props: NotificationListProps) => { return ; }; +const IconSidebar = createIcon( + 'IconSidebar', + , +); + +const SidebarToggle = () => { + const { closeSidebar, openSidebar, sidebarOpen } = useSidebar(); + const { t } = useTranslationContext(); + return ( + + ); +}; + const language = new URLSearchParams(window.location.search).get('language'); const i18nInstance = language ? new Streami18n({ language: language as any }) : undefined; @@ -195,7 +229,7 @@ const App = () => { () => appSettingsStore.getLatestValue().panelLayout, [], ); - const initialNavOpen = useMemo(() => { + const initialSidebarOpen = useMemo(() => { if (typeof window === 'undefined') return !initialPanelLayout.leftPanel.collapsed; const isMobile = window.innerWidth < DESKTOP_LAYOUT_BREAKPOINT; @@ -324,7 +358,7 @@ const App = () => { ); } @@ -358,46 +392,49 @@ const App = () => { MessageReactions: CustomMessageReactions, reactionOptions: newReactionOptions, Search: CustomChannelSearch, + HeaderEndContent: SidebarToggle, + HeaderStartContent: SidebarToggle, ...messageUiOverrides, }} > - -
+ - - - - - - - + + - -
-
+ + + + + + + + + ); }; diff --git a/examples/vite/src/ChatLayout/Panels.tsx b/examples/vite/src/ChatLayout/Panels.tsx index 441e019cc2..958197bd12 100644 --- a/examples/vite/src/ChatLayout/Panels.tsx +++ b/examples/vite/src/ChatLayout/Panels.tsx @@ -23,6 +23,7 @@ import { } from 'stream-chat-react'; import { SidebarResizeHandle, ThreadResizeHandle } from './Resize.tsx'; +import { useSidebar } from './SidebarContext.tsx'; import { ThreadStateSync } from './Sync.tsx'; const ChannelThreadPanel = () => { @@ -92,7 +93,8 @@ export const ChannelsPanels = ({ options: ChannelOptions; sort: ChannelSort; }) => { - const { channel, navOpen = true } = useChatContext('ChannelsPanels'); + const { channel } = useChatContext('ChannelsPanels'); + const { sidebarOpen } = useSidebar(); const channelsLayoutRef = useRef(null); return ( @@ -100,7 +102,7 @@ export const ChannelsPanels = ({
@@ -138,7 +140,7 @@ export const ThreadsPanels = ({ iconOnly?: boolean; itemSet?: ChatViewSelectorEntry[]; }) => { - const { navOpen = true } = useChatContext('ThreadsPanels'); + const { sidebarOpen } = useSidebar(); const { activeThread } = useThreadsViewContext(); const threadsLayoutRef = useRef(null); @@ -148,7 +150,7 @@ export const ThreadsPanels = ({
diff --git a/examples/vite/src/ChatLayout/Resize.tsx b/examples/vite/src/ChatLayout/Resize.tsx index 4f3746b569..3d5e21af82 100644 --- a/examples/vite/src/ChatLayout/Resize.tsx +++ b/examples/vite/src/ChatLayout/Resize.tsx @@ -6,7 +6,7 @@ import { useEffect, useRef, } from 'react'; -import { useChatContext } from 'stream-chat-react'; +import { useSidebar } from './SidebarContext.tsx'; import { type LeftPanelLayoutSettingsState, @@ -197,7 +197,7 @@ const PanelResizeHandle = ({ ); export const SidebarLayoutSync = () => { - const { navOpen = true } = useChatContext(); + const { sidebarOpen } = useSidebar(); const { collapsed: leftPanelCollapsed } = useAppSettingsSelector( (state) => state.panelLayout.leftPanel, ); @@ -209,7 +209,7 @@ export const SidebarLayoutSync = () => { if (document.body.classList.contains('app-chat-resizing-sidebar')) return; - const shouldBeCollapsed = !navOpen; + const shouldBeCollapsed = !sidebarOpen; if (shouldBeCollapsed === leftPanelCollapsed) return; @@ -220,7 +220,7 @@ export const SidebarLayoutSync = () => { collapsed: shouldBeCollapsed, }, })); - }, [leftPanelCollapsed, navOpen]); + }, [leftPanelCollapsed, sidebarOpen]); return null; }; @@ -230,7 +230,7 @@ export const SidebarResizeHandle = ({ }: { layoutRef: RefObject; }) => { - const { closeMobileNav, openMobileNav } = useChatContext('SidebarResizeHandle'); + const { closeSidebar, openSidebar } = useSidebar(); const leftPanel = useAppSettingsSelector((state) => state.panelLayout.leftPanel); const isSidebarCollapsedRef = useRef(leftPanel.collapsed); const leftPanelStateRef = useRef(leftPanel); @@ -301,9 +301,9 @@ export const SidebarResizeHandle = ({ isSidebarCollapsedRef.current = shouldCollapse; if (shouldCollapse) { - closeMobileNav(); + closeSidebar(); } else { - openMobileNav(); + openSidebar(); } } }, @@ -330,7 +330,7 @@ export const SidebarResizeHandle = ({ pointerId: event.pointerId, }); }, - [closeMobileNav, layoutRef, openMobileNav], + [closeSidebar, layoutRef, openSidebar], ); return ( diff --git a/examples/vite/src/ChatLayout/SidebarContext.tsx b/examples/vite/src/ChatLayout/SidebarContext.tsx new file mode 100644 index 0000000000..6472a570ee --- /dev/null +++ b/examples/vite/src/ChatLayout/SidebarContext.tsx @@ -0,0 +1,32 @@ +import { createContext, useCallback, useContext, useState } from 'react'; +import type { PropsWithChildren } from 'react'; + +type SidebarContextValue = { + closeSidebar: () => void; + openSidebar: () => void; + sidebarOpen: boolean; +}; + +const SidebarContext = createContext(undefined); + +export const useSidebar = () => { + const value = useContext(SidebarContext); + if (!value) throw new Error('useSidebar must be used within a SidebarProvider'); + return value; +}; + +export const SidebarProvider = ({ + children, + initialOpen = true, +}: PropsWithChildren<{ initialOpen?: boolean }>) => { + const [sidebarOpen, setSidebarOpen] = useState(initialOpen); + + const closeSidebar = useCallback(() => setSidebarOpen(false), []); + const openSidebar = useCallback(() => setSidebarOpen(true), []); + + return ( + + {children} + + ); +}; diff --git a/examples/vite/src/LoadingScreen/LoadingScreen.tsx b/examples/vite/src/LoadingScreen/LoadingScreen.tsx index a3d00b1a4e..8a34149507 100644 --- a/examples/vite/src/LoadingScreen/LoadingScreen.tsx +++ b/examples/vite/src/LoadingScreen/LoadingScreen.tsx @@ -10,7 +10,7 @@ import { LoadingChannel, LoadingChannels } from 'stream-chat-react'; type LoadingScreenProps = { initialAppLayoutStyle: CSSProperties; initialChannelSelected: boolean; - initialNavOpen: boolean; + initialSidebarOpen: boolean; }; const selectorButtonCount = 4; @@ -18,7 +18,7 @@ const selectorButtonCount = 4; export const LoadingScreen = ({ initialAppLayoutStyle, initialChannelSelected, - initialNavOpen, + initialSidebarOpen, }: LoadingScreenProps) => (
@@ -27,16 +27,11 @@ export const LoadingScreen = ({
-
+
{Array.from({ length: selectorButtonCount }).map((_, index) => (
))}
-
+
diff --git a/examples/vite/src/index.scss b/examples/vite/src/index.scss index 527e85bddc..e285aaa1da 100644 --- a/examples/vite/src/index.scss +++ b/examples/vite/src/index.scss @@ -308,11 +308,22 @@ body { 0s, 0s, 0s, 0s, 0s, 0s, var(--str-chat__channel-list-transition-duration, 180ms); } - .app-chat-sidebar-overlay > .str-chat__chat-view__selector, - .app-chat-sidebar-overlay - > .str-chat__chat-view__selector.str-chat__chat-view__selector--nav-closed, - .app-chat-sidebar-overlay - > .str-chat__chat-view__selector.str-chat__chat-view__selector--nav-open { + /* Hide expand toggle in content headers when sidebar is visible */ + .app-chat-view__channels-layout:not( + .app-chat-view__channels-layout--sidebar-collapsed + ) + .str-chat__channel-header + .str-chat__header-sidebar-toggle { + display: none; + } + + .app-chat-view__threads-layout:not(.app-chat-view__threads-layout--sidebar-collapsed) + .str-chat__thread-header + .str-chat__header-sidebar-toggle { + display: none; + } + + .app-chat-sidebar-overlay > .str-chat__chat-view__selector { position: static; inset: auto; width: var(--str-chat__chat-view-selector-mobile-width); diff --git a/src/components/Button/ToggleSidebarButton.tsx b/src/components/Button/ToggleSidebarButton.tsx deleted file mode 100644 index 6f44eccbae..0000000000 --- a/src/components/Button/ToggleSidebarButton.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -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 ? ( -