import { lazy, Suspense, useEffect } from "react"; import { Route, Switch, Redirect, useLocation } from "wouter"; import { useSession } from "@/store/session"; import { useMail } from "@/store/mail"; import { useContacts } from "@/store/contacts"; import { useCalendar } from "@/store/calendar"; import { useFiles } from "@/store/files"; import { useSieve } from "@/store/sieve"; import { push } from "@/jmap/push"; import { client } from "@/jmap/client"; import { ToastHost } from "@/ui/toast"; import { ConfirmHost } from "@/ui/dialog"; import { Spinner } from "@/ui/misc"; import { LoginPage } from "@/views/Login"; import { AppShell } from "@/views/AppShell"; import { MailView } from "@/views/mail/MailView"; import { ComposerDock } from "@/views/compose/ComposerDock"; import { setUnreadBadge } from "@/lib/notify"; import { useSettings } from "@/store/settings"; const ContactsView = lazy(() => import("@/views/contacts/ContactsView").then((m) => ({ default: m.ContactsView }))); const CalendarView = lazy(() => import("@/views/calendar/CalendarView").then((m) => ({ default: m.CalendarView }))); const FilesView = lazy(() => import("@/views/files/FilesView").then((m) => ({ default: m.FilesView }))); const SettingsView = lazy(() => import("@/views/settings/SettingsView").then((m) => ({ default: m.SettingsView }))); export function App() { const status = useSession((s) => s.status); const bootstrap = useSession((s) => s.bootstrap); useEffect(() => { void bootstrap(); }, [bootstrap]); if (status === "loading") { return (
); } return ( <> {status === "anonymous" ? : } ); } function AuthedApp() { const accountId = useSession((s) => s.accountId); const [location] = useLocation(); // Initial data + push wiring useEffect(() => { if (!accountId) return; const mail = useMail.getState(); void mail.loadMailboxes(); void mail.loadIdentities(); void mail.loadQuota(); void useContacts.getState().init(); void useCalendar.getState().init(); void useFiles.getState().init(); void useSieve.getState().init(); push.start(); const pending = new Map>(); let timer: number | null = null; const unsub = push.subscribe((acct, type) => { const set = pending.get(acct) ?? new Set(); set.add(type); pending.set(acct, set); if (timer) return; timer = window.setTimeout(() => { timer = null; for (const [a, types] of pending) { if (a === useMail.getState().accountId) void useMail.getState().applyChanges(types); if (a === useContacts.getState().accountId) useContacts.getState().applyChanges(types); if (a === useCalendar.getState().accountId) useCalendar.getState().applyChanges(types); if (a === useFiles.getState().accountId) useFiles.getState().applyChanges(types); if (a === useSieve.getState().accountId) useSieve.getState().applyChanges(types); } pending.clear(); }, 400); }); const unsubState = client.onSessionState(() => void useSession.getState().refresh()); // Poll fallback when push is disconnected (every 2 minutes) const poll = window.setInterval(() => { if (!push.connected && document.visibilityState === "visible") { void useMail.getState().applyChanges(new Set(["Email", "Mailbox"])); } }, 120_000); return () => { unsub(); unsubState(); window.clearInterval(poll); push.stop(); }; }, [accountId]); // Unread badge in title/favicon const inboxUnread = useMail((s) => { const id = s.roleId("inbox"); return id ? (s.mailboxes[id]?.unreadEmails ?? 0) : 0; }); const appName = useSession((s) => s.session?.ihasmail?.appName ?? "ihasmail"); useEffect(() => { void import("@/lib/notify").then((m) => { m.setBaseTitle(appName); setUnreadBadge(inboxUnread); }); }, [inboxUnread, appName]); // Request notification permission lazily when enabled const notif = useSettings((s) => s.settings.desktopNotifications); useEffect(() => { if (notif) void import("@/lib/notify").then((m) => m.requestNotificationPermission()); }, [notif]); return ( }> {(p) => } {(p) => } {(p) => } {(p) => } {(p) => } {(p) => } {location === "/" ? : } ); }