mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 16:16:59 +00:00
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.
70 lines
2.2 KiB
TypeScript
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));
|
|
}
|