Files
webmail/components/protocol/launch-file-host.tsx
T
Linus Rath c65d18eaae feat: open mail, calendar and contact files from the OS and share into a new draft
The installed app only handled mailto: and webcal: launches. The launch
consumer dropped files, and nothing could hand a File to the composer or
the import dialogs, because the pending mailto is JSON in sessionStorage.

- manifest: `shortcuts` named in the browser's language, `file_handlers`
  for .eml, .ics and .vcf, and `share_target`, all with the base path and
  per-domain branding. Lite's manifest is unchanged.
- `lib/launch-handoff.ts`: the one place files and links wait, in memory
  and in the Cache API, so they survive a sign-in that leaves the page.
  The desktop bridge can call the same functions, since launchQueue never
  fires in Electron. Sign-out clears it.
- launch consumer: routes files by type, and takes a shortcut to an open
  window, which focus-existing would otherwise only focus.
- .eml opens in a new host for the preview, also before sign-in, with the
  strictest device policy deciding about saving. .ics and .vcf open their
  import dialogs through a new `initialFile` prop. A .ics needs the server,
  so offline the dialog says so and carries on once back online.
- composer: `initialFiles` goes through `addFiles`, so plugin vetoes and
  size limits still apply, also from a Pro compose tab.
- `sw.js` takes the share POST before the offline check and keeps the files
  for the page. Without a worker, `/protocol/share` passes text and links
  on and the page says the files are lost. `protocol` joins the app
  sections and the handler pages join the shell, so a mailto: launch
  offline no longer gets a network error.

Browsers without launchQueue or a service worker behave as before.
2026-11-03 05:26:01 +01:00

63 lines
2.2 KiB
TypeScript

"use client";
import { useCallback, useEffect, useState, type ComponentType, type ComponentProps } from "react";
import { useTranslations } from "next-intl";
import type { FilePreviewModal as FilePreviewModalType } from "@/components/files/file-preview-modal";
import { useHandoffFiles } from "@/hooks/use-handoff-files";
import { saveBlobAs } from "@/lib/download-guard";
import { toast } from "@/stores/toast-store";
type PreviewComponent = ComponentType<ComponentProps<typeof FilePreviewModalType>>;
/**
* Shows a .eml opened from the OS. It parses in the browser, so it works
* offline and before sign-in. `policyAccount` stays out: the strictest device
* policy of the accounts signed in here decides about saving (FilePreviewModal
* and saveBlobAs check it).
*
* The preview is loaded when a file arrives, to keep it off the layout's
* bundle, and by hand rather than with next/dynamic: without a connection and
* without the offline shell its chunk can't load, which has to end in a
* message, not in the error page, and work again later.
*/
export function LaunchFileHost() {
const t = useTranslations("pwaLaunch");
const { file, done } = useHandoffFiles("eml", true);
const [Preview, setPreview] = useState<PreviewComponent | null>(null);
useEffect(() => {
if (!file || Preview) return;
let stale = false;
import("@/components/files/file-preview-modal")
.then((module) => {
if (!stale) setPreview(() => module.FilePreviewModal);
})
.catch(() => {
if (stale) return;
toast.error(t("open_failed"));
done();
});
return () => { stale = true; };
}, [file, Preview, done, t]);
const getFileContent = useCallback(async () => {
if (!file) throw new Error("no file");
return { blob: file, contentType: "message/rfc822" };
}, [file]);
const onDownload = useCallback(() => {
if (file) saveBlobAs(file, file.name, { kind: "attachment" });
}, [file]);
if (!file || !Preview) return null;
return (
<Preview
key={`${file.name}:${file.lastModified}`}
name={file.name}
onClose={done}
onDownload={onDownload}
getFileContent={getFileContent}
/>
);
}