import { useEffect, useState, type ReactNode } from "react";
import { Link, useLocation } from "wouter";
import { BookOpen, Calendar, ChevronsUpDown, FolderOpen, Globe, HelpCircle, LogOut, Mail, Menu as MenuIcon, Moon, PenSquare, Plus, RefreshCw, Settings, Sun, Upload, Users } from "lucide-react";
import { useSession } from "@/store/session";
import { toggleTarget, 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, Popover, useMenu } from "@/ui/popover";
import { SearchBar } from "./SearchBar";
import { MailboxTree } from "./mail/MailboxTree";
import { FilesTree } from "./files/FilesTree";
import { ContactsSidebar } from "./contacts/ContactsSidebar";
import { CalendarSidebar } from "./calendar/CalendarSidebar";
import { ShortcutsDialog, useGlobalShortcuts } from "./Shortcuts";
import { formatSize } from "@/lib/format";
import { TranslateBoundary } from "@/ui/TranslateBoundary";
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 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]);
/*
* There is no account switcher any more.
*
* It existed to reach what other people shared, and was the wrong door: it
* moved the whole app to somebody else's account, and Stalwart advertises
* every capability on a shared account, so mail, calendar and contacts went
* with it and were refused. Shares are listed where they belong now -- in
* Files and in Contacts, beside the reader's own -- and found without anyone
* having to know an account switch was involved.
*/
return (
{/* A product name, not a word. "ihasmail" translated is a different
product, and the one on the tab beside it is still called this. */}
ihasmail
{session?.username}
{session?.ihasmail?.loginName}
} label="Documentation" href="https://docs.ihasmail.org" external />
{/* The project site. It is linked from the login screen footer, which
is a page a signed-in user never sees again -- so from inside the
app there was no way back to it. */}
} label="About ihasmail" href="https://ihasmail.org" external />
} label="Settings" onClick={() => navigate("/settings")} />
} label="Refresh" onClick={() => window.location.reload()} />
} label="Sign out" onClick={() => void logout()} />
setDrawer(false)} />
{/*
Scoped to the content, not the shell. If Chrome's translator breaks a
message list, the top bar, the folder tree and any open composer are
outside this and carry on -- so recovery is a pane blinking rather
than the app disappearing.
*/}
{children}
);
}
/**
* Flip to light and back from the top bar.
*
* The setting has four values and only two of them are "light", so the button
* acts on what is actually on screen rather than on the setting: if you can
* see a dark theme, one click gives you light.
*
* Coming back is the part that needs remembering. There is more than one way
* to be dark — "dark", "ihasmail", or "system" while the OS is — so the way
* back is whichever you were on, kept in `lastDarkTheme`, rather than plain
* "dark" for everyone. Without that, two clicks would quietly move an
* ihasmail user onto a theme they never chose.
*/
function ThemeToggle() {
const effective = useEffectiveTheme();
const lastDarkTheme = useSettings((s) => s.settings.lastDarkTheme);
const update = useSettings((s) => s.update);
const next = toggleTarget(effective, lastDarkTheme);
// The label names where you are going, and going back is not always "dark"
// any more -- it is whichever theme you were on before flipping to light.
const label = next === "light" ? "light mode" : next === "system" ? "your system theme" : next === "ihasmail" ? "the ihasmail theme" : "dark mode";
return (
);
}