import { useEffect, useMemo, useState } from "react"; import { useTimeSkew } from "../hooks/useTimeSkew"; import { config } from "../config"; function formatDelay(ms: number, withSign = true) { const sign = withSign ? (ms > 0 ? "+" : ms < 0 ? "−" : "") : ""; const abs = Math.abs(ms); if (abs >= 1000) { const s = Math.floor(abs / 1000); const rem = abs % 1000; return `${sign}${s}s${rem ? ` ${rem}ms` : ""}`; } return `${sign}${abs}ms`; } export default function TimeSyncNotice({ thresholdMs = 500, intervalMs, lang = "ja", dismissTtlMs = 5 * 60_000, // ✅ reappear after 5 minutes by default storageScope = "session", // "session" | "local" }: { thresholdMs?: number; intervalMs?: number; lang?: "ja" | "en"; dismissTtlMs?: number; storageScope?: "session" | "local"; }) { const { skewMs, rttMs, recheck, error } = useTimeSkew({ intervalMs: intervalMs ?? config.intervals.timeSkewMs }); const KEY = "timesync.dismissedUntil"; const store = useMemo( () => (storageScope === "local" ? window.localStorage : window.sessionStorage), [storageScope], ); // read dismissed state (true if now < dismissedUntil) const [dismissed, setDismissed] = useState(() => { try { const raw = store.getItem(KEY); const until = raw ? Number(raw) : 0; return Number.isFinite(until) && Date.now() < until; } catch { return false; } }); // auto-unset when TTL passes (while component stays mounted) useEffect(() => { if (!dismissed) return; let id: number | null = null; try { const raw = store.getItem(KEY); const until = raw ? Number(raw) : 0; const delay = Math.max(0, until - Date.now()); if (delay > 0) { id = window.setTimeout(() => setDismissed(false), delay); } else { setDismissed(false); store.removeItem(KEY); } } catch { // ignore storage errors } return () => { if (id) clearTimeout(id); }; }, [dismissed, store]); // if skew returns within threshold, clear dismissal so it can show again later if it drifts useEffect(() => { if (skewMs != null && Math.abs(skewMs) <= thresholdMs && dismissed) { setDismissed(false); try { store.removeItem(KEY); } catch { void 0; } } }, [skewMs, thresholdMs, dismissed, store]); if (dismissed) return null; const hasSkew = skewMs != null; const skewVal = skewMs ?? 0; if (!error && (!hasSkew || Math.abs(skewVal) <= thresholdMs)) return null; const ahead = skewVal > 0; const msgJa = error ? `時刻同期に失敗しました: ${error}` : ahead ? `端末の時計が正確な時刻より ${formatDelay(skewVal)} 進んでいます(通信往復遅延 ${rttMs ?? "-"}ms)` : `端末の時計が正確な時刻より ${formatDelay(-skewVal)} 遅れています(通信往復遅延 ${rttMs ?? "-"}ms)`; const msgEn = error ? `Time sync failed: ${error}` : ahead ? `Your device clock is ${formatDelay(skewVal)} ahead of the correct time (RTT ${rttMs ?? "-"}ms).` : `Your device clock is ${formatDelay(-skewVal)} behind the correct time (RTT ${rttMs ?? "-"}ms).`; const onClose = () => { setDismissed(true); try { const until = Date.now() + dismissTtlMs; store.setItem(KEY, String(until)); } catch { // ignore storage failures } }; return (
{lang === "ja" ? msgJa : msgEn}
); }