"use client"; /** * useOfflineStatus — the live input source for the R2 UI (OfflineBanner + * StatusChip). Feeds the pure `computeOfflineStatus` (lib/offline-status) * with: * - `navigator.onLine` (+ the window `online`/`offline` events), * - the connection phase (the legacy JSON `realtime` client today — the * Yjs `RealtimeProvider` is wired in TASK-0136, and its `localPendingCount` * slots into `crdtPending` then), * - the outbox pending/failed counts (the `offlineQueue` store). * The phase-since timestamp (the 5 s blip basis) is tracked on phase change. */ import { useEffect, useMemo, useRef, useState } from "react"; import { realtime } from "@/lib/realtime"; import { computeOfflineStatus, normalizePhase, type ConnectionPhase, } from "@/lib/offline-status"; import { useOutboxStore } from "@/stores/offlineQueue"; export interface OfflineStatusView { bannerVisible: boolean; pendingCount: number; failedCount: number; showReviewLink: boolean; chip: ConnectionPhase; reconnectFlash: boolean; ariaLabel: string; } export function useOfflineStatus(): OfflineStatusView { const [online, setOnline] = useState(() => typeof navigator !== "undefined" ? navigator.onLine : true ); const [phase, setPhase] = useState(() => normalizePhase(realtime.connectionStatus)); const phaseSinceMs = useRef(Date.now()); const [phaseSince, setPhaseSince] = useState(phaseSinceMs.current); const [nowMs, setNowMs] = useState(() => Date.now()); const outboxPending = useOutboxStore((s) => s.pending); const outboxFailed = useOutboxStore((s) => s.failed); // Re-evaluate the 5 s blip threshold once per second (the banner flips from // hidden → visible exactly at the threshold; a 1 s granularity is plenty). useEffect(() => { const id = setInterval(() => setNowMs(Date.now()), 1_000); return () => clearInterval(id); }, []); useEffect(() => { if (typeof window === "undefined") return; const onUp = () => setOnline(true); const onDown = () => setOnline(false); window.addEventListener("online", onUp); window.addEventListener("offline", onDown); return () => { window.removeEventListener("online", onUp); window.removeEventListener("offline", onDown); }; }, []); useEffect(() => { const off = realtime.onStatus((status) => { const next = normalizePhase(status); setPhase((prev) => { if (prev !== next) { phaseSinceMs.current = Date.now(); setPhaseSince(phaseSinceMs.current); } return next; }); }); return off; }, []); return useMemo( () => computeOfflineStatus({ navigatorOnline: online, phase, phaseSinceMs: phaseSince, nowMs, crdtPending: 0, // 0 until the Yjs provider is wired (TASK-0136) outboxPending, outboxFailed }), [online, phase, phaseSince, nowMs, outboxPending, outboxFailed] ); }