Files
webmail/components/error/app-error-boundary.tsx
T
Linus Rath bb2393cf04 feat: open messages and drafts in their own windows and save all attachments to a folder
An installed app had no title bar of its own, no way to save attachments
except a zip, and "pop out" stayed inside the page. A new window was a
second copy of the app that had to restore the session itself, and on
start-up that evicts any login made without "Remember me" from the list
every window shares.

- Title bar: `display_override` with `window-controls-overlay`, one
  `--titlebar-inset` variable at the page roots and skeletons, and a drag
  strip with the window title. 0 without an overlay, so nothing moves.
- Save all to a folder: `showDirectoryPicker`, asking the device policy
  before the picker and before each file, with the zip as the fallback.
- Pop-out routes `/popout/message/<id>` and `/popout/compose`, on offer in
  an installed app or when the desktop bridge has windows. Hidden in Lite.
- A pop-out borrows the main window's session and never evicts or starts a
  token timer. Every renewal now takes a Web Lock, so two windows can't
  send the same refresh token, which Stalwart 1.0 may treat as a replay.
- A draft moves to its window under the same id. A closing window hands
  unsaved text to the main window to save, since its own request wouldn't
  survive the close.
- Read state, drafts, theme and language follow across windows, and
  `html[data-desktop]` gates the app-like behaviour in `app/desktop.css`.
2026-11-03 06:50:21 +01:00

40 lines
1.5 KiB
TypeScript

"use client";
import type { ReactNode } from "react";
import { NavigationRail } from "@/components/layout/navigation-rail";
import { useIsMobile } from "@/hooks/use-media-query";
import { ErrorBoundary, type FallbackProps } from "./error-boundary";
import { PageErrorFallback, SettingsErrorFallback } from "./error-fallbacks";
function AppErrorLayout({ kind, ...props }: FallbackProps & { kind: "page" | "settings" }) {
const isMobile = useIsMobile();
const Fallback = kind === "settings" ? SettingsErrorFallback : PageErrorFallback;
// The navigation stays, so one app crashing doesn't strand the user in it.
return (
<div className="flex h-dvh bg-background pt-[var(--app-top-inset)]">
{!isMobile && (
<div className="w-14 bg-secondary flex flex-col flex-shrink-0 border-e border-border">
<NavigationRail collapsed />
</div>
)}
<div className="flex flex-col flex-1 min-w-0">
<Fallback {...props} />
{isMobile && <NavigationRail orientation="horizontal" />}
</div>
</div>
);
}
/**
* Wraps one app (Calendar, Contacts, Files, Settings): a crash in it shows
* the shared error fallback next to the navigation, instead of the route
* error page in place of everything.
*/
export function AppErrorBoundary({ children, kind = "page" }: { children: ReactNode; kind?: "page" | "settings" }) {
return (
<ErrorBoundary fallback={(props) => <AppErrorLayout kind={kind} {...props} />}>
{children}
</ErrorBoundary>
);
}