diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 416d1c64..5e462ea4 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,5 +1,4 @@ -import { useEffect, useLayoutEffect, useRef, useState, type MouseEvent, type ReactNode } from "react"; -import { diagnosticsEnabled } from "./live/diagnostics"; +import { Suspense, useEffect, useLayoutEffect, useState, type ReactNode } from "react"; import { Link, Redirect, Route, Switch, useLocation, useSearch } from "wouter"; import { LoginPage } from "./auth/Login"; import { SignupPage, VerifyEmailPage } from "./auth/Signup"; @@ -7,35 +6,15 @@ import { ForgotPasswordPage, ResetPasswordPage } from "./auth/ForgotPassword"; import { InvitePage, OnboardPage } from "./auth/InviteOnboard"; import { PrivacyPage, TermsPage } from "./legal/LegalPages"; import { useApp } from "./state"; -import { Header, MoreSheet, PhoneTabs, Sidebar } from "./shell/Shell"; -import { HomeEmptyScreen, HomeScreen, PlacesScreen } from "./screens/Home"; -import { HubEmptyScreen, HubScreen } from "./screens/Hub"; -import { - ExpensesEmptyScreen, - ExpensesScreen, - FundsScreen, - RecurringScreen, - SettleEvenScreen, - SettleScreen, -} from "./screens/Money"; -import { - ActivityScreen, - MembersScreen, - NotesScreen, - TemplatesScreen, -} from "./screens/House"; -import { OperationsScreen } from "./screens/Operations"; -import { CommuneSettingsScreen, SettingsScreen } from "./screens/Settings"; -import { Windows } from "./windows/Windows"; -import { cx } from "./ui/cx"; -import { PAGE_TITLES, type ScreenId } from "./data/household"; -import { COMMUNE_SCREENS, forgetReturn, keepSearch, readAddress, readReturn, rememberReturn } from "./nav"; +import { forgetReturn, keepSearch, readAddress, readReturn, rememberReturn } from "./nav"; import { AuthLayout } from "./auth/AuthLayout"; import { PendingShell } from "./shell/PendingShell"; -import { AllActivityScreen, SearchScreen } from "./screens/Search"; import { ConnectionRecovery } from "./shell/ConnectionRecovery"; -import { RecoveryBoundary, ScreenRecovery } from "./shell/Recovery"; -import { isConnectionFailure } from "./live/connection"; +import { lazyComponent } from "./shell/lazyComponent"; +import { usePageTitle } from "./shell/pageTitle"; + +// The signed-in app, so that the sign-in pages do not download every screen. +const ProductShell = lazyComponent(() => import("./shell/ProductShell").then((m) => m.ProductShell)); export function App() { return ( @@ -98,12 +77,6 @@ export function App() { ); } -// "Expenses · Flat 4 · Commune": short, most specific first. -function usePageTitle(...parts: Array) { - const title = [...parts.filter(Boolean), "Commune"].join(" · "); - useEffect(() => { document.title = title; }, [title]); -} - function PageTitle({ title }: { title: string }) { usePageTitle(title); return null; @@ -127,9 +100,13 @@ function SignedIn({ hasHubs }: { hasHubs: boolean }) { function ProtectedApp() { const [path] = useLocation(); + const known = Boolean(readAddress(path).screen); + // Fetched while the session is checked; until it arrives, the same + // loading page stays up. + useEffect(() => { if (known) ProductShell.preload().catch(() => {}); }, [known]); // An address Commune does not have needs no sign-in to say so. - if (!readAddress(path).screen) return <>; - return ; + if (!known) return <>; + return }>; } function AuthenticatedOnly({ children }: { children: ReactNode }) { @@ -169,192 +146,3 @@ function AppUnavailable() { const { refresh, signOut } = useApp(); return ; } - -function ProductShell() { - const { screen, navMode, moreOpen, native, dark, window: win, notes, noteKey, apiError, clearApiError, contentError, hub, refresh, searchTarget, unknownCommune } = useApp(); - const noteReader = screen === "notes" && win === "note" && Boolean(notes[noteKey]); - const modalOpen = Boolean(win) && !noteReader; - usePageFocus(screen, hub.id, Boolean(win) || Boolean(searchTarget)); - usePageTitle(unknownCommune ? "Commune not found" : (win && PAGE_TITLES[win]) || PAGE_TITLES[screen], - COMMUNE_SCREENS.has(screen) && !unknownCommune ? hub.name : undefined); - return ( -
-
- Skip to content - -
-
- {diagnosticsEnabled() ? : null} - {apiError ?
{isConnectionFailure(apiError) ? <>Connection interrupted. Your last loaded view is still available. : apiError}
: null} - {contentError && hub.activityState === "ready" ?
Could not refresh this commune. Showing its last loaded activity.
: null} -
- }> - - -
-
-
-
- - -
- -
- ); -} - -// Focus moves to an element that only holds it until the next step away. -function focusOnce(element: HTMLElement) { - if (!element.hasAttribute("tabindex")) { - element.tabIndex = -1; - element.addEventListener("blur", () => element.removeAttribute("tabindex"), { once: true }); - } - element.focus({ preventScroll: true }); -} - -function skipToContent(event: MouseEvent) { - const main = document.getElementById("main-content"); - if (!main) return; - event.preventDefault(); - focusOnce(main); -} - -// After moving to another page or commune, start at the new page's heading -// rather than on a control that has gone. A window, the note reader or a -// search result places focus itself; if one was opening, wait until it closes -// and only step in when focus would otherwise be lost. -function usePageFocus(screen: ScreenId, hubId: string, held: boolean) { - const shown = useRef({ screen, hubId }); - const waiting = useRef(false); - useEffect(() => { - const previous = shown.current; - shown.current = { screen, hubId }; - const moved = previous.screen !== screen || (Boolean(previous.hubId) && previous.hubId !== hubId); - if (moved) waiting.current = true; - if (!waiting.current || held) return; - waiting.current = false; - const active = document.activeElement; - if (!moved && active && active !== document.body) return; - const heading = document.querySelector(".a-header-title h1"); - if (heading) focusOnce(heading); - }, [screen, hubId, held]); -} - -function ConnectionCheck() { - const { hubId } = useApp(); - const [running, setRunning] = useState(false); - const [result, setResult] = useState(""); - async function check() { - setRunning(true); - setResult(""); - try { - const { inspectMembershipRead } = await import("./live/neon"); - const rows = []; - for (let round = 1; round <= 5; round++) { - rows.push({ round, reads: await inspectMembershipRead(hubId) }); - } - setResult(JSON.stringify(rows, null, 2)); - } catch { - setResult("Connection test could not complete."); - } finally { setRunning(false); } - } - return
- Read-only connection diagnostics - -
{result}
-
; -} - -function Screen({ screen }: { screen: ScreenId }) { - return ( -
- -
- ); -} - -function ScreenBody({ screen }: { screen: ScreenId }) { - const { session, emptyHub, hub, contentLoading, contentError, refresh, unknownCommune, go } = useApp(); - const hasHubs = Boolean(session?.hasHubs); - const hasSettlement = hub.moves.some((move) => !move.even); - - // Never another commune's records in its place. - if (unknownCommune) { - return

This commune isn’t available to you.

You may have left it, or the link is for a different account.

; - } - - if (contentLoading && screen !== "settings" && screen !== "search") { - if (contentError) return

Could not load {hub.name}

{contentError}

; - return
Loading shared records{screen === "hub" &&

{hub.name}

{hub.switchMeta}

}