import { useEffect, useState, type ReactNode } from "react"; import { Link, useLocation } from "wouter"; import { Calendar, ChevronsUpDown, FolderOpen, HelpCircle, Mail, Menu as MenuIcon, Moon, PenSquare, Settings, Sun, Users, LogOut, Plus, RefreshCw } from "lucide-react"; import { useSession } from "@/store/session"; import { useEffectiveTheme, useSettings } from "@/store/settings"; import { useMail } from "@/store/mail"; import { draftFromMailto, useCompose } from "@/store/compose"; import { Avatar, useIsMobile } from "@/ui/misc"; import { MenuItem, MenuSep, MenuTitle, Popover, useMenu } from "@/ui/popover"; import { SearchBar } from "./SearchBar"; import { MailboxTree } from "./mail/MailboxTree"; import { CalendarSidebar } from "./calendar/CalendarSidebar"; import { ShortcutsDialog, useGlobalShortcuts } from "./Shortcuts"; import { formatSize } from "@/lib/format"; import { CAP } from "@/jmap/client"; const PUSH_LABEL = { connected: "Live updates connected", connecting: "Live updates reconnecting…", disconnected: "Live updates off — checking periodically instead", } as const; export function AppShell({ children }: { children: ReactNode }) { const [location, navigate] = useLocation(); const isMobile = useIsMobile(); const collapsed = useSettings((s) => s.settings.sidebarCollapsed); const update = useSettings((s) => s.update); const [drawer, setDrawer] = useState(false); const [helpOpen, setHelpOpen] = useState(false); const openCompose = useCompose((s) => s.open); const pushState = useSession((s) => s.pushState); const session = useSession((s) => s.session); const accountId = useSession((s) => s.accountId); const setAccount = useSession((s) => s.setAccount); const logout = useSession((s) => s.logout); const acctMenu = useMenu(); const section = location.split("/")[1] || "mail"; useGlobalShortcuts({ onHelp: () => setHelpOpen(true) }); useEffect(() => setDrawer(false), [location]); // Deep link: /mail?compose=new (PWA shortcut) / mailto handler useEffect(() => { const params = new URLSearchParams(window.location.search); if (params.get("compose") === "new") { openCompose(); navigate("/mail", { replace: true }); } const mailto = params.get("mailto"); if (mailto) { openCompose(draftFromMailto(mailto)); navigate("/mail", { replace: true }); } }, [openCompose, navigate]); const accounts = session ? Object.entries(session.accounts) : []; const mailAccounts = accounts.filter(([, a]) => CAP.mail in (a.accountCapabilities ?? {})); return (
ihasmail{mailAccounts.length > 1 ? "" : ""}
{session?.username}
{session?.ihasmail?.loginName}
{mailAccounts.length > 1 && ( <> Accounts {mailAccounts.map(([id, a]) => ( setAccount(id)} /> ))} )} } label="Settings" onClick={() => navigate("/settings")} /> } label="Refresh" onClick={() => window.location.reload()} /> } label="Sign out" onClick={() => void logout()} />
setDrawer(false)} />
{children}
{isMobile && ( <> {(section === "mail" || section === "search") && !location.split("/")[3] && ( )} )} setHelpOpen(false)} />
); } /** Outlook-style module switcher at the bottom of the folder pane. */ function ModuleLink({ href, icon, label, active }: { href: string; icon: ReactNode; label: string; active: boolean }) { return ( {icon} {label} ); } function QuotaBar() { const quotas = useMail((s) => s.quotas); const q = quotas.find((x) => x.resourceType === "octets" && x.types.includes("Email")) ?? quotas.find((x) => x.resourceType === "octets"); if (!q || !q.hardLimit) return null; const pct = Math.min(100, Math.round((q.used / q.hardLimit) * 100)); return (
{formatSize(q.used)} of {formatSize(q.hardLimit)}
95 ? "danger" : pct > 80 ? "warn" : ""} style={{ width: `${pct}%` }} />
); } /** * Flip between light and dark from the top bar. * * The stored setting has a third value, "system", so the button acts on what * is actually on screen rather than on the setting: whichever theme you can * see, one click gives you the other one. Choosing "match system" again lives * in Settings › Appearance, where the three-way choice belongs. */ function ThemeToggle() { const effective = useEffectiveTheme(); const update = useSettings((s) => s.update); const next = effective === "dark" ? "light" : "dark"; return ( ); }