diff options
| author | soryu <soryu@soryu.co> | 2026-04-29 01:10:11 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-29 01:10:11 +0100 |
| commit | 4b1d608b839769052634b4facc345b891d468926 (patch) | |
| tree | 1d5ff45b5b34b2e3e378a4cf69fd62ff39cf12de /makima/frontend/src/hooks/useUserSettings.ts | |
| parent | 5bde7c2d7e099fd9c8b2615602ab1d096bd9b6be (diff) | |
| download | soryu-4b1d608b839769052634b4facc345b891d468926.tar.gz soryu-4b1d608b839769052634b4facc345b891d468926.zip | |
feat: document-mode directive UI proof of concept (Lexical) (#101)
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Backend: feature flag + goal-edit interrupt messaging
* WIP: heartbeat checkpoint
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Frontend: Lexical document editor with step blocks, context menu, countdown
Diffstat (limited to 'makima/frontend/src/hooks/useUserSettings.ts')
| -rw-r--r-- | makima/frontend/src/hooks/useUserSettings.ts | 115 |
1 files changed, 115 insertions, 0 deletions
diff --git a/makima/frontend/src/hooks/useUserSettings.ts b/makima/frontend/src/hooks/useUserSettings.ts new file mode 100644 index 0000000..b39244d --- /dev/null +++ b/makima/frontend/src/hooks/useUserSettings.ts @@ -0,0 +1,115 @@ +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 <SettingsPage> +// will reactively update <DirectivesPage> if it's mounted, and vice versa. +let cachedSettings: UserSettings | null = null; +let inflight: Promise<void> | null = null; +const subscribers = new Set<(s: UserSettings | null) => void>(); + +function notify() { + for (const sub of subscribers) sub(cachedSettings); +} + +function loadOnce(): Promise<void> { + 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<UserSettings>) => Promise<UserSettings>; + /** Force a refresh from the server (e.g. after sign-in). */ + refresh: () => Promise<void>; +} + +/** + * 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<UserSettings | null>(cachedSettings); + const [loading, setLoading] = useState<boolean>(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<UserSettings>): Promise<UserSettings> => { + 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 }; +} |
