mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
120 lines
4.3 KiB
TypeScript
120 lines
4.3 KiB
TypeScript
import { locales } from '@/i18n/routing';
|
|
import { LITE_PENDING_PATH_KEY } from '@/lib/lite';
|
|
import type { AppSurface } from '@/lib/deep-links';
|
|
|
|
/**
|
|
* Deep-link segments for the static Lite build.
|
|
*
|
|
* The exported HTML for `/en/mail/` is what a static host serves for every
|
|
* path below it (`/en/mail/thread/abc`), so the route params Next prerendered
|
|
* are always empty. The surfaces recover the segments from the address bar
|
|
* instead, with exactly the shape Next would have passed: the path after
|
|
* `/<prefix>/<locale>/<surface>/`, split and percent-decoded once.
|
|
*
|
|
* Hosts without rewrite rules (GitHub Pages) answer such a URL with 404.html.
|
|
* The not-found page parks the requested path in sessionStorage and jumps to
|
|
* the surface root; `takePendingLitePath` hands it over once so the surface
|
|
* still opens the linked item and restores the URL.
|
|
*/
|
|
|
|
const SURFACES: readonly AppSurface[] = ['mail', 'calendar', 'contacts', 'files', 'settings'];
|
|
|
|
function stripPrefix(pathname: string, prefix: string): string {
|
|
if (prefix && (pathname === prefix || pathname.startsWith(`${prefix}/`))) {
|
|
return pathname.slice(prefix.length) || '/';
|
|
}
|
|
return pathname;
|
|
}
|
|
|
|
function decodeOnce(segment: string): string {
|
|
try {
|
|
return decodeURIComponent(segment);
|
|
} catch {
|
|
return segment;
|
|
}
|
|
}
|
|
|
|
/** Splits a browser pathname into locale, surface and the remaining segments. */
|
|
export function parseLitePath(
|
|
pathname: string,
|
|
prefix = '',
|
|
): { locale: string | null; surface: AppSurface | null; segments: string[] } {
|
|
const parts = stripPrefix(pathname.split('?')[0].split('#')[0], prefix)
|
|
.split('/')
|
|
.filter(Boolean);
|
|
let locale: string | null = null;
|
|
if (parts.length > 0 && (locales as readonly string[]).includes(parts[0])) {
|
|
locale = parts.shift() as string;
|
|
}
|
|
const head = parts[0];
|
|
if (!head) return { locale, surface: 'mail', segments: [] };
|
|
if ((SURFACES as readonly string[]).includes(head)) {
|
|
return { locale, surface: head as AppSurface, segments: parts.slice(1).map(decodeOnce) };
|
|
}
|
|
return { locale, surface: null, segments: [] };
|
|
}
|
|
|
|
/**
|
|
* Segments for `surface` derived from `pathname`, or `[]` when the URL points
|
|
* somewhere else (the root, another surface, the Pro shell).
|
|
*/
|
|
export function liteSegmentsFromPath(pathname: string, surface: AppSurface, prefix = ''): string[] {
|
|
const parsed = parseLitePath(pathname, prefix);
|
|
return parsed.surface === surface ? parsed.segments : [];
|
|
}
|
|
|
|
/** The surface root a parked deep link should be replayed on, or null. */
|
|
export function liteSurfaceRootFor(pathname: string, prefix = ''): string | null {
|
|
const parsed = parseLitePath(pathname, prefix);
|
|
if (!parsed.locale || !parsed.surface) return null;
|
|
return `${prefix}/${parsed.locale}/${parsed.surface}/`;
|
|
}
|
|
|
|
/** Parks a deep link for the surface root to pick up (404 shim). */
|
|
export function stashPendingLitePath(pathWithSearch: string): boolean {
|
|
try {
|
|
sessionStorage.setItem(LITE_PENDING_PATH_KEY, pathWithSearch);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads a parked deep link if it belongs to `surface`, leaving it in place.
|
|
* Anything parked for another surface is ignored so its own shell can pick it
|
|
* up. Safe to call from a render (or a `useState` initializer): React may
|
|
* discard that render attempt when a sibling suspends (contacts reads
|
|
* `useSearchParams` under a Suspense boundary) and run it again, so the entry
|
|
* must survive until `clearPendingLitePath` runs from a committed effect.
|
|
*/
|
|
export function peekPendingLitePath(surface: AppSurface, prefix = ''): string | null {
|
|
try {
|
|
const pending = sessionStorage.getItem(LITE_PENDING_PATH_KEY);
|
|
if (!pending) return null;
|
|
const parsed = parseLitePath(pending, prefix);
|
|
return parsed.surface === surface ? pending : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Removes the parked deep link once the surface has committed to it. */
|
|
export function clearPendingLitePath(): void {
|
|
try {
|
|
sessionStorage.removeItem(LITE_PENDING_PATH_KEY);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Consumes a parked deep link if it belongs to `surface` (peek + clear in one
|
|
* step, for callers that run outside a React render).
|
|
*/
|
|
export function takePendingLitePath(surface: AppSurface, prefix = ''): string | null {
|
|
const pending = peekPendingLitePath(surface, prefix);
|
|
if (pending) clearPendingLitePath();
|
|
return pending;
|
|
}
|