import { useCallback, useEffect, useState } from "react"; import { getUserSettings, updateUserSettings, type UserSettings, } from "../lib/api"; const DEFAULT_SETTINGS: UserSettings = { documentModeEnabled: false }; // Module-level cache + pub-sub so multiple components mounting the hook stay // in sync without a full provider/context. Toggling the flag in // will reactively update if it's mounted, and vice versa. let cachedSettings: UserSettings | null = null; let inflight: Promise | null = null; const subscribers = new Set<(s: UserSettings | null) => void>(); function notify() { for (const sub of subscribers) sub(cachedSettings); } function loadOnce(): Promise { if (inflight) return inflight; inflight = getUserSettings() .then((s) => { cachedSettings = s; notify(); }) .catch((err) => { // Swallow but log — fall back to safe defaults so the existing UI keeps // rendering even if /settings endpoint is unavailable. console.error("Failed to load user settings:", err); cachedSettings = DEFAULT_SETTINGS; notify(); }) .finally(() => { inflight = null; }); return inflight; } export interface UseUserSettingsResult { /** Loaded settings, or null while loading for the first time. */ settings: UserSettings | null; /** True while the initial GET is in flight. */ loading: boolean; /** Update one or more settings; persists via PUT and updates the cache. */ update: (patch: Partial) => Promise; /** Force a refresh from the server (e.g. after sign-in). */ refresh: () => Promise; } /** * React hook for the per-user settings record (feature flags). * * Calls GET /api/v1/users/me/settings on first mount and caches the result. * Subsequent mounts read from the cache. `update()` PUTs to the server and * notifies all live subscribers so UI gates reactively flip without a reload. */ export function useUserSettings(): UseUserSettingsResult { const [settings, setSettings] = useState(cachedSettings); const [loading, setLoading] = useState(cachedSettings === null); useEffect(() => { let mounted = true; const sub = (s: UserSettings | null) => { if (!mounted) return; setSettings(s); setLoading(false); }; subscribers.add(sub); if (cachedSettings === null) { loadOnce(); } else { // Already cached — make sure local state matches. setSettings(cachedSettings); setLoading(false); } return () => { mounted = false; subscribers.delete(sub); }; }, []); const update = useCallback( async (patch: Partial): Promise => { const base = cachedSettings ?? DEFAULT_SETTINGS; const merged: UserSettings = { ...base, ...patch }; // Optimistic update so the toggle flips immediately. cachedSettings = merged; notify(); try { const result = await updateUserSettings(merged); cachedSettings = result; notify(); return result; } catch (err) { // Roll back to last-known-good on failure. cachedSettings = base; notify(); throw err; } }, [], ); const refresh = useCallback(async () => { cachedSettings = null; notify(); await loadOnce(); }, []); return { settings, loading, update, refresh }; }