Files
fusion/packages/dashboard/app/hooks/visibilitySuspension.ts
Fusion a2258b811f fix(FN-3838): suppress suspended-tab chat load failure toasts
- Add visibility suspension hook to detect tab-hidden periods during chat loading
- Update useChat and useQuickChat to suppress load-failed toasts triggered by tab suspension resumes
- Expand hook test coverage for suspension timing and toast suppression behavior
- Add plugin authoring documentation updates and include FN-3838 changeset

Fusion-Task-Id: FN-3838
2026-05-09 10:31:05 -07:00

69 lines
1.9 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef } from "react";
const SUSPENSION_ERROR_PATTERNS = [
"load failed",
"failed to fetch",
"networkerror when attempting to fetch resource.",
"connection aborted",
"connection closed unexpectedly",
"network error",
];
export function isLikelyTabSuspensionError(message: string): boolean {
const normalized = message.trim().toLowerCase();
if (!normalized) {
return false;
}
return SUSPENSION_ERROR_PATTERNS.some((pattern) => normalized.includes(pattern));
}
export function useTabVisibilitySuspension() {
const lastHiddenAtRef = useRef<number | null>(null);
const lastVisibleAtRef = useRef<number | null>(null);
useEffect(() => {
if (typeof document === "undefined") {
return;
}
const handleVisibilityChange = () => {
const now = Date.now();
if (document.visibilityState === "hidden") {
lastHiddenAtRef.current = now;
return;
}
if (document.visibilityState === "visible") {
lastVisibleAtRef.current = now;
}
};
handleVisibilityChange();
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
}, []);
const isHiddenNow = useCallback(() => typeof document !== "undefined" && document.visibilityState === "hidden", []);
const wasRecentlyHidden = useCallback((windowMs = 5000): boolean => {
const hiddenAt = lastHiddenAtRef.current;
if (hiddenAt === null) {
return false;
}
const now = Date.now();
if (isHiddenNow()) {
return now - hiddenAt <= windowMs;
}
const visibleAt = lastVisibleAtRef.current;
if (visibleAt === null || visibleAt < hiddenAt) {
return false;
}
return now - visibleAt <= windowMs;
}, [isHiddenNow]);
return useMemo(() => ({
isHiddenNow,
wasRecentlyHidden,
}), [isHiddenNow, wasRecentlyHidden]);
}