diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 436c277..dcd926a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,7 +5,7 @@ import ShowsPage from "./pages/ShowsPage"; import TimeSyncNotice from "./components/TimeSyncNotice"; import "./index.css"; -const TIME_SYNC_OFF_THRESHOLD = 500; +const TIME_SYNC_OFF_THRESHOLD = 200; export default function App() { const [open, setOpen] = React.useState(false); diff --git a/frontend/src/components/TimeSyncNotice.tsx b/frontend/src/components/TimeSyncNotice.tsx index e9cd815..57f97dc 100644 --- a/frontend/src/components/TimeSyncNotice.tsx +++ b/frontend/src/components/TimeSyncNotice.tsx @@ -16,22 +16,57 @@ 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"; }) { - // removed `error` const { skewMs, rttMs, recheck } = useTimeSkew({ intervalMs }); + const KEY = "timesync.dismissedUntil"; + const store = storageScope === "local" ? window.localStorage : window.sessionStorage; + + // read dismissed state (true if now < dismissedUntil) const [dismissed, setDismissed] = useState(() => { - try { return sessionStorage.getItem("timesync.dismissed") === "1"; } catch { return false; } + 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 */ + } + return () => { if (id) clearTimeout(id); }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dismissed]); + + // 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 { sessionStorage.removeItem("timesync.dismissed"); } catch { } + try { store.removeItem(KEY); } catch { } } }, [skewMs, thresholdMs, dismissed]); @@ -47,7 +82,10 @@ export default function TimeSyncNotice({ const onClose = () => { setDismissed(true); - try { sessionStorage.setItem("timesync.dismissed", "1"); } catch { } + try { + const until = Date.now() + dismissTtlMs; + store.setItem(KEY, String(until)); + } catch { } }; return (