import { useEffect, useState } from 'react'; import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom'; import { useVoiceStore } from '../stores/voice.ts'; import { useAuthStore, type UserStatus } from '../stores/auth.ts'; import { useWebSocketStore } from '../stores/ws.ts'; import { useServerStore } from '../stores/server.ts'; import { useChannelStore } from '../stores/channel.ts'; import { useLayoutStore } from '../stores/layout.ts'; import { ServerBar } from './ServerBar.tsx'; import { ChannelList } from './ChannelList.tsx'; import { ConversationList } from './ConversationList.tsx'; import { NotificationPrompt } from './NotificationPrompt.tsx'; import { MemberList } from './MemberList.tsx'; import { VoicePanel } from './VoicePanel.tsx'; import { ServerSettingsModal } from './ServerSettingsModal.tsx'; const STATUS_CYCLE: UserStatus[] = ['online', 'idle', 'dnd', 'offline']; function statusColor(status: UserStatus): string { switch (status) { case 'online': return 'bg-gb-green'; case 'idle': return 'bg-gb-yellow'; case 'dnd': return 'bg-gb-red'; default: return 'bg-gb-gray'; } } function statusLabel(status: UserStatus): string { return status.toUpperCase(); } export function Layout() { const isAuthenticated = useAuthStore((state) => state.isAuthenticated); const isLoading = useAuthStore((state) => state.isLoading); const user = useAuthStore((state) => state.user); const logout = useAuthStore((state) => state.logout); const updateProfile = useAuthStore((state) => state.updateProfile); const wsConnect = useWebSocketStore((s) => s.connect); const wsDisconnect = useWebSocketStore((s) => s.disconnect); const navigate = useNavigate(); const location = useLocation(); const [showStatusMenu, setShowStatusMenu] = useState(false); const isDM = useLayoutStore((s) => s.isDM); const mobileView = useLayoutStore((s) => s.mobileView); const setMobileView = useLayoutStore((s) => s.setMobileView); const [showServerSettings, setShowServerSettings] = useState(false); const activeServerId = useServerStore((s) => s.activeServerId); const currentVoiceRoom = useVoiceStore((s) => s.currentRoom); const [activeTab, setActiveTab] = useState<'chat' | 'voice'>('chat'); useEffect(() => { if (currentVoiceRoom) setActiveTab('voice'); else setActiveTab('chat'); }, [currentVoiceRoom]); useEffect(() => { wsConnect(); return () => { wsDisconnect(); }; }, [wsConnect, wsDisconnect]); useEffect(() => { if (!isLoading && !isAuthenticated && location.pathname !== '/login') { navigate('/login', { replace: true }); } }, [isLoading, isAuthenticated, location.pathname, navigate]); // Navigate to chat view on mobile when a channel/DM is selected useEffect(() => { const unsub = useChannelStore.subscribe((state, prev) => { if (state.activeChannelId !== prev.activeChannelId && state.activeChannelId) { setMobileView('chat'); } }); return unsub; }, [setMobileView]); useEffect(() => { if (location.pathname.startsWith('/dm/') && location.pathname !== '/dm') { setMobileView('chat'); } }, [location.pathname, setMobileView]); const handleStatusChange = async (status: UserStatus) => { setShowStatusMenu(false); try { await updateProfile({ status }); } catch { /* store surfaces error */ } }; const currentStatus = user?.status ?? 'offline'; if (isLoading) { return
[booting...]
; } if (!isAuthenticated) return null; return (
{/* Outer terminal frame with safe area */}
{/* Top bar — minimal on mobile */}
DUMPSTER
{showStatusMenu && (
{STATUS_CYCLE.map((s) => ( ))}
)}
[SETTINGS] {activeServerId && ( )}
{/* Main content */}
{/* Sidebar — desktop: always visible, mobile: overlay when sidebar tab active */}
{isDM ? : } {/* Close overlay on mobile by tapping background */}
setMobileView('chat')} />
{/* Chat */}
{currentVoiceRoom && (
)}
{currentVoiceRoom && (
)}
{/* Members — desktop: right column, mobile: overlay when members tab active */} {!isDM && (
setMobileView('chat')} />
)}
{/* Bottom nav — mobile only */} {showServerSettings && activeServerId && ( setShowServerSettings(false)} /> )} {/* Status bar — desktop only */}
TERM {__APP_VERSION__} {new Date().toISOString().slice(0, 10)}
); } // MobileBottomNav — tab bar for switching between sidebar / chat / members function MobileBottomNav({ mobileView, onViewChange, isDM, }: { mobileView: string; onViewChange: (v: 'sidebar' | 'chat' | 'members') => void; isDM: boolean; }) { const isConnected = useVoiceStore((s) => s.isConnected); return (
onViewChange('sidebar')}> [SERVERS] onViewChange('chat')}> [CHAT] {!isDM && ( onViewChange('members')}> [MEMBERS] )} {isConnected && (
)}
); } function NavTab({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) { return ( ); }