From 8463ca9ba8c922327c0da8162741ec53ad045640 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:32:52 +0100 Subject: [PATCH] feat(mobile): add MobileShell and split AppLayout for mobile/desktop --- .../web/src/components/layout/AppLayout.tsx | 40 +++++++- .../web/src/components/layout/MobileShell.tsx | 94 +++++++++++++++++++ 2 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 packages/web/src/components/layout/MobileShell.tsx diff --git a/packages/web/src/components/layout/AppLayout.tsx b/packages/web/src/components/layout/AppLayout.tsx index fc20f069..5d9e7716 100644 --- a/packages/web/src/components/layout/AppLayout.tsx +++ b/packages/web/src/components/layout/AppLayout.tsx @@ -4,7 +4,7 @@ import { SpaceSidebar } from './SpaceSidebar'; import { ChannelSidebar } from './ChannelSidebar'; import { MainContent } from './MainContent'; import { RightPanel } from './RightPanel'; -import { MobileNav } from './MobileNav'; +import { MobileShell } from './MobileShell'; import { ImagePreview } from '../chat/ImagePreview'; import { CreateSpaceModal } from '../modals/CreateSpace'; import { JoinSpaceModal } from '../modals/JoinSpace'; @@ -267,9 +267,39 @@ export function AppLayout() { ); } + // ── Mobile layout ── + if (isMobile) { + return ( + <> + + {/* Modals still render globally for both mobile and desktop */} + + + + + + {/* UserSettings is a pushed screen on mobile (MobileSettingsScreen), not a modal */} + + + + + + + + + + + + + + + ); + } + + // ── Desktop layout ── return (
- {/* Space sidebar - always visible on desktop, toggled on mobile */} + {/* Space sidebar - always visible on desktop */}
@@ -304,12 +334,12 @@ export function AppLayout() { {/* User Profile Popout */} {userProfilePopout.user && userProfilePopout.position && ( <> -
- diff --git a/packages/web/src/components/layout/MobileShell.tsx b/packages/web/src/components/layout/MobileShell.tsx new file mode 100644 index 00000000..94f380db --- /dev/null +++ b/packages/web/src/components/layout/MobileShell.tsx @@ -0,0 +1,94 @@ +import React, { useEffect } from 'react'; +import { useUIStore } from '../../stores/uiStore'; +import { useVoiceStore } from '../../stores/voiceStore'; +import { useLocation } from 'react-router-dom'; +import { MobileScreenStack } from './MobileScreenStack'; +import { MobileBottomNav } from './MobileBottomNav'; + +// Screen components — imported as they are created in subsequent tasks. +// Inline placeholders are used until the real components exist. +const PlaceholderScreen = ({ label }: { label: string }) => ( +
+ {label} +
+); + +// These will be replaced with real imports as each task is completed: +const MobileSpacesScreen = () => ; +const MobileDmsScreen = () => ; +const MobileYouScreen = () => ; +const MobileChatScreen = ({ params: _params }: { params?: Record }) => ; +const MobileSettingsScreen = ({ initialPanel: _p }: { initialPanel?: string }) => ; +const MobileVoiceMiniBar = () => null; +const MobileVoiceFullScreen = () => ; + +const screenMap: Record) => React.ReactNode> = { + 'channel-chat': (params) => , + 'friends': () => , + 'settings': () => , + 'settings-account': () => , + 'settings-voice': () => , + 'settings-privacy': () => , + 'settings-connections': () => , + 'settings-instance': () => , + 'members': () => , + 'voice-full': () => , + 'explore': () => , + 'user-profile': () => , +}; + +export function MobileShell() { + const mobileScreen = useUIStore((s) => s.mobileScreen); + const popMobileScreen = useUIStore((s) => s.popMobileScreen); + const pushMobileScreen = useUIStore((s) => s.pushMobileScreen); + const mobileStack = useUIStore((s) => s.mobileStack); + const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId); + const location = useLocation(); + + // Reconstruct mobile stack from URL on mount (deep link / refresh support) + useEffect(() => { + const path = location.pathname; + const match = path.match(/^\/channels\/([^/]+)\/([^/]+)$/); + if (match && mobileStack.length === 0) { + const [, spaceId, channelId] = match; + if (spaceId === '@me') { + pushMobileScreen('channel-chat', { channelId, spaceId: '@me' }); + } else { + pushMobileScreen('channel-chat', { channelId, spaceId }); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // Only on mount + + // Sync browser back button with mobile stack + useEffect(() => { + const handlePopState = () => { + if (useUIStore.getState().mobileStack.length > 0) { + popMobileScreen(); + } + }; + window.addEventListener('popstate', handlePopState); + return () => window.removeEventListener('popstate', handlePopState); + }, [popMobileScreen]); + + const rootScreens: Record = { + spaces: , + dms: , + you: , + }; + + return ( +
+ + + {/* Voice mini-bar — shown when in a voice call */} + {currentVoiceChannelId && } + + {/* Bottom nav — MobileBottomNav hides itself when stack is non-empty */} + +
+ ); +}