mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
93 lines
3.6 KiB
TypeScript
93 lines
3.6 KiB
TypeScript
import type { AppSurface } from '@/lib/deep-links';
|
|
|
|
/**
|
|
* Handoff for a deep link that has to survive a route change (#733).
|
|
*
|
|
* The Pro shell owns a single route (`/pro`) and renders each surface inside a
|
|
* tab, so a permalink like `/mail/message/<id>` can't reach the surface as a
|
|
* route param: ProInterfaceRedirect intercepts it and replaces the URL. It
|
|
* parks the segments here, opens the right tab, and the surface picks them up
|
|
* on mount - exactly like `protocol-handlers/session` does for `mailto:`.
|
|
*
|
|
* Some links carry a query-string half too (`?account=` for calendar events,
|
|
* `?preview=` for files). Under `/pro` the address bar has no search string
|
|
* left by the time the surface parses the link, so it travels with the parked
|
|
* entry instead of being read back from `window.location`.
|
|
*
|
|
* Surfaces in the Pro shell stay mounted for the whole session, so a link that
|
|
* arrives *while the shell is already running* (an in-app link to
|
|
* `/settings/folders`, a `mailto:`-style handoff, ...) would be parked and
|
|
* never seen - the mount-time consume has long since run. For that case a
|
|
* mounted surface subscribes here: when a listener exists, `setPendingDeepLink`
|
|
* delivers the segments to it directly instead of parking them.
|
|
*
|
|
* Module state, deliberately: it must not survive a reload (the URL is gone by
|
|
* then, so re-applying it would reopen something the user already closed).
|
|
*/
|
|
export interface PendingDeepLink {
|
|
segments: string[];
|
|
/** Query-string half of the link (leading `?` included), when it has one. */
|
|
search?: string;
|
|
}
|
|
|
|
const pending = new Map<AppSurface, PendingDeepLink>();
|
|
|
|
type PendingDeepLinkListener = (segments: string[], search?: string) => void;
|
|
|
|
const listeners = new Map<AppSurface, Set<PendingDeepLinkListener>>();
|
|
|
|
export function setPendingDeepLink(surface: AppSurface, segments: string[], search?: string): void {
|
|
const live = listeners.get(surface);
|
|
if (live && live.size > 0) {
|
|
// A mounted surface is listening - deliver instead of parking, so links
|
|
// arriving while the Pro shell is already running still land.
|
|
// No trailing undefined: listeners predating the search half see the
|
|
// exact call shape they always did.
|
|
for (const listener of [...live]) {
|
|
if (search === undefined) listener(segments);
|
|
else listener(segments, search);
|
|
}
|
|
return;
|
|
}
|
|
pending.set(surface, { segments, search });
|
|
}
|
|
|
|
/** Returns the parked link once, then forgets it. */
|
|
export function consumePendingDeepLinkEntry(surface: AppSurface): PendingDeepLink | null {
|
|
const entry = pending.get(surface);
|
|
if (!entry) return null;
|
|
pending.delete(surface);
|
|
return entry;
|
|
}
|
|
|
|
/** Segments-only variant for surfaces whose links never carry a search string. */
|
|
export function consumePendingDeepLink(surface: AppSurface): string[] | null {
|
|
return consumePendingDeepLinkEntry(surface)?.segments ?? null;
|
|
}
|
|
|
|
/**
|
|
* Live delivery for an already-mounted surface (the Pro shell keeps surfaces
|
|
* mounted for the whole session). Returns the unsubscribe function. Only the
|
|
* embedded (Pro) instance of a surface should subscribe - during a cold-load
|
|
* redirect the standard instance briefly renders too and must not steal the
|
|
* link parked for the Pro instance that mounts right after.
|
|
*/
|
|
export function subscribePendingDeepLink(
|
|
surface: AppSurface,
|
|
listener: PendingDeepLinkListener,
|
|
): () => void {
|
|
let set = listeners.get(surface);
|
|
if (!set) {
|
|
set = new Set();
|
|
listeners.set(surface, set);
|
|
}
|
|
set.add(listener);
|
|
return () => {
|
|
set.delete(listener);
|
|
};
|
|
}
|
|
|
|
export function clearPendingDeepLinks(): void {
|
|
pending.clear();
|
|
}
|