summaryrefslogtreecommitdiff
path: root/makima/frontend/src/hooks/useUserSettings.ts
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-04-29 01:10:11 +0100
committerGitHub <noreply@github.com>2026-04-29 01:10:11 +0100
commit4b1d608b839769052634b4facc345b891d468926 (patch)
tree1d5ff45b5b34b2e3e378a4cf69fd62ff39cf12de /makima/frontend/src/hooks/useUserSettings.ts
parent5bde7c2d7e099fd9c8b2615602ab1d096bd9b6be (diff)
downloadsoryu-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.ts115
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 };
+}