Feature/protocol handlers
* Added account selection for protocol links when multiple connected accounts are available, including mailto: links * Added support for handling mailto: links in an already-open PWA/session instead of always opening a new tab * Added webcal: protocol handling for calendar links * Added account selection for webcal: links when multiple calendar-capable accounts are connected * Added an import-or-subscribe choice for detected webcal calendars * Added protocol handler settings for registering mail and calendar handlers and choosing the open mode * Added service worker/session coordination for passing protocol requests between browser/PWA contexts * Added tests and translations for the new protocol handler flows
This commit is contained in:
committed by
Linus Rath
parent
8b0e2052cf
commit
3f444a8912
@@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { usePathname, useRouter } from "@/i18n/navigation";
|
||||
import { getPathPrefix } from "@/lib/browser-navigation";
|
||||
import { parseMailto } from "@/lib/protocol-handlers/mailto";
|
||||
import { parseWebcal } from "@/lib/protocol-handlers/webcal";
|
||||
import {
|
||||
listenForMailtoRequests,
|
||||
notifyPendingMailto,
|
||||
notifyPendingWebcal,
|
||||
requestOpenMailtoInExistingClient,
|
||||
savePendingMailto,
|
||||
savePendingWebcal,
|
||||
} from "@/lib/protocol-handlers/session";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
|
||||
type LaunchParams = { targetURL?: string };
|
||||
type StandaloneNavigator = Navigator & { standalone?: boolean };
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
launchQueue?: {
|
||||
setConsumer: (consumer: (launchParams: LaunchParams) => void) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getProtocolLaunch(targetURL: string):
|
||||
| { kind: "mailto"; raw: string }
|
||||
| { kind: "webcal"; raw: string }
|
||||
| null {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(targetURL, window.location.origin);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (url.origin !== window.location.origin) return null;
|
||||
|
||||
const raw = url.searchParams.get("url");
|
||||
if (!raw) return null;
|
||||
|
||||
if (url.pathname.includes("/protocol/mailto")) return { kind: "mailto", raw };
|
||||
if (url.pathname.includes("/protocol/webcal")) return { kind: "webcal", raw };
|
||||
return null;
|
||||
}
|
||||
|
||||
function isStandaloneDisplayMode() {
|
||||
return window.matchMedia?.("(display-mode: standalone)").matches
|
||||
|| (navigator as StandaloneNavigator).standalone === true;
|
||||
}
|
||||
|
||||
function openProtocolInNewTab(protocol: "mailto" | "webcal", raw: string): boolean {
|
||||
const url = `${getPathPrefix()}/protocol/${protocol}?url=${encodeURIComponent(raw)}&fallback=1`;
|
||||
const opened = window.open(url, "_blank");
|
||||
if (!opened) return false;
|
||||
opened.opener = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
interface ProtocolLaunchHandlerProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ProtocolLaunchHandlerProvider({ children }: ProtocolLaunchHandlerProviderProps) {
|
||||
const t = useTranslations("protocol_handlers");
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
if (pathname.startsWith("/protocol/")) return;
|
||||
|
||||
return listenForMailtoRequests((pending) => {
|
||||
savePendingMailto(pending);
|
||||
notifyPendingMailto();
|
||||
if (pathname !== "/") router.push("/");
|
||||
}, () => ({
|
||||
path: pathname,
|
||||
standalone: isStandaloneDisplayMode(),
|
||||
focusNotificationTitle: t("focus_notification_title"),
|
||||
focusNotificationBody: t("focus_notification_body"),
|
||||
}));
|
||||
}, [pathname, router, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined" || !window.launchQueue) return;
|
||||
|
||||
window.launchQueue.setConsumer((launchParams) => {
|
||||
if (!launchParams.targetURL) return;
|
||||
|
||||
const launch = getProtocolLaunch(launchParams.targetURL);
|
||||
if (!launch) return;
|
||||
|
||||
if (launch.kind === "mailto") {
|
||||
const parsed = parseMailto(launch.raw);
|
||||
if (!parsed) return;
|
||||
|
||||
if (useSettingsStore.getState().protocolOpenMode === "new-tab") {
|
||||
if (openProtocolInNewTab("mailto", launch.raw)) return;
|
||||
savePendingMailto(parsed);
|
||||
notifyPendingMailto();
|
||||
if (pathname !== "/") router.push("/");
|
||||
return;
|
||||
}
|
||||
|
||||
void requestOpenMailtoInExistingClient(parsed).then((delivered) => {
|
||||
if (delivered) return;
|
||||
savePendingMailto(parsed);
|
||||
notifyPendingMailto();
|
||||
if (pathname !== "/") router.push("/");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseWebcal(launch.raw);
|
||||
if (!parsed) return;
|
||||
|
||||
if (useSettingsStore.getState().protocolOpenMode === "new-tab") {
|
||||
if (openProtocolInNewTab("webcal", launch.raw)) return;
|
||||
}
|
||||
|
||||
savePendingWebcal(parsed);
|
||||
notifyPendingWebcal();
|
||||
if (pathname !== "/calendar") router.push("/calendar");
|
||||
});
|
||||
}, [pathname, router]);
|
||||
|
||||
return children;
|
||||
}
|
||||
Reference in New Issue
Block a user