1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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 };
}
|