Files
webmail/hooks/use-wopi-status.ts
T
dealerweb 3eb9ade572 fix: send the WOPI status and launch requests under the base path
Behind a reverse proxy at a sub-path (NEXT_PUBLIC_BASE_PATH, e.g.
/webmail) both WOPI requests used a bare fetch('/api/wopi/...') and
reached the mail server instead of the webmail. The status probe came
back 404, so office editing always looked unavailable there - and since
#1047 the probe runs for every opened message, logging the 404 each
time. Route both through apiFetch like every other API call, and cover
the sub-path mount with a test.
2026-09-24 21:08:17 +02:00

70 lines
2.2 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { IS_LITE } from "@/lib/lite";
import { apiFetch } from "@/lib/browser-navigation";
/**
* Whether a WOPI document editor is configured for this deployment and which
* file extensions it handles (#425). Fetched once per page load and shared
* across consumers via a module-level promise.
*/
export interface WopiStatus {
enabled: boolean;
editExtensions: string[];
viewExtensions: string[];
}
const DISABLED: WopiStatus = { enabled: false, editExtensions: [], viewExtensions: [] };
let statusPromise: Promise<WopiStatus> | null = null;
function fetchWopiStatus(): Promise<WopiStatus> {
// The WOPI bridge is a server feature; the static build never has an editor.
if (IS_LITE) return Promise.resolve(DISABLED);
if (!statusPromise) {
statusPromise = apiFetch("/api/wopi/status")
.then((res) => (res.ok ? res.json() : DISABLED))
.then((data) => ({
enabled: !!data?.enabled,
editExtensions: Array.isArray(data?.editExtensions) ? data.editExtensions : [],
viewExtensions: Array.isArray(data?.viewExtensions) ? data.viewExtensions : [],
}))
.catch(() => {
statusPromise = null; // allow a retry on the next mount
return DISABLED;
});
}
return statusPromise;
}
export function useWopiStatus(enabled: boolean): WopiStatus | null {
const [status, setStatus] = useState<WopiStatus | null>(null);
useEffect(() => {
if (!enabled) return;
let cancelled = false;
fetchWopiStatus().then((s) => {
if (!cancelled) setStatus(s);
});
return () => {
cancelled = true;
};
}, [enabled]);
return status;
}
/** Lowercased extension of a file name, without the dot. */
export function fileExtension(name: string): string {
const parts = name.toLowerCase().split(".");
return parts.length > 1 ? parts.pop() || "" : "";
}
/** Whether the configured editor can open (edit or view) a file of this name. */
export function canWopiOpen(status: WopiStatus | null, name?: string | null): boolean {
if (!status?.enabled || !name) return false;
const ext = fileExtension(name);
return !!ext && (status.editExtensions.includes(ext) || status.viewExtensions.includes(ext));
}