- H2 useAdminModeration now fetches the 'cleared' label too, so an admin Undo actually propagates (newest-verdict-wins) instead of only working on-device. - H3 admin takedowns publish the NIP-32 'hidden' label (useAdminHide) instead of an author-only kind-5 relays ignore; the cross-app hide is real now. - H5 the /stats outage banner works: queryCanonical reports relay failures via onError, so 'partial' can be true and zeros are never presented as truth. - H6 useShops got its own cache namespace (was colliding with useSkateShops). - H7 listing NIP-05 is verified against the user's key, not merely self-asserted. - H8 NIP-22 spot comments/transfer now use uppercase root tags (A/K/P); readers accept legacy #a; NIP.md corrected. - H9 Lab profile edits preserve foreign spot refs (crew link = 35006 only). - H10 collection edits preserve foreign NIP-51 tags. - H11 LAB_KINDS = all 13 provider kinds (was 35001-35005). - H16 skip-to-content is a button that focuses <main> (was href=#main, which HashRouter treated as a route → 404). - H17 CustomizeNavSheet is translated; useLocalStorage re-reads on key change and keeps stateRef in sync cross-tab. - Backup 'confirmed' flag is now per-account (backup-confirmed:<pubkey>).
101 lines
3.3 KiB
TypeScript
101 lines
3.3 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
/**
|
|
* Generic hook for managing localStorage state.
|
|
*
|
|
* The setter is **referentially stable** (and safe against stale closures): it
|
|
* keeps the latest value in a ref and updates via a functional `setState`. This
|
|
* matters because several callers put the setter (directly or through a chain of
|
|
* callbacks) in an effect's dependency array — an unstable identity there causes
|
|
* the effect to re-run every render, and if it also sets state that is an
|
|
* infinite render loop.
|
|
*/
|
|
export function useLocalStorage<T>(
|
|
key: string,
|
|
defaultValue: T,
|
|
serializer?: {
|
|
serialize: (value: T) => string;
|
|
deserialize: (value: string) => T;
|
|
}
|
|
) {
|
|
// Keep the serializer in a ref so `serialize`/`deserialize` stay stable even
|
|
// when callers pass an inline object literal on every render.
|
|
const serializerRef = useRef(serializer);
|
|
serializerRef.current = serializer;
|
|
const serialize = useCallback(
|
|
(value: T) => (serializerRef.current?.serialize ?? JSON.stringify)(value),
|
|
[],
|
|
);
|
|
const deserialize = useCallback(
|
|
(value: string) => (serializerRef.current?.deserialize ?? JSON.parse)(value),
|
|
[],
|
|
);
|
|
|
|
const [state, setState] = useState<T>(() => {
|
|
try {
|
|
const item = localStorage.getItem(key);
|
|
return item ? deserialize(item) : defaultValue;
|
|
} catch (error) {
|
|
console.warn(`Failed to load ${key} from localStorage:`, error);
|
|
return defaultValue;
|
|
}
|
|
});
|
|
|
|
// Mirror the latest state so the stable setter can support functional updates
|
|
// without a stale closure.
|
|
const stateRef = useRef(state);
|
|
stateRef.current = state;
|
|
|
|
const defaultValueRef = useRef(defaultValue);
|
|
defaultValueRef.current = defaultValue;
|
|
|
|
// Re-read when the key CHANGES (e.g. a per-account key once the user logs in
|
|
// or switches accounts). The lazy initializer only runs on mount.
|
|
const prevKeyRef = useRef(key);
|
|
useEffect(() => {
|
|
if (prevKeyRef.current === key) return;
|
|
prevKeyRef.current = key;
|
|
try {
|
|
const item = localStorage.getItem(key);
|
|
const next = item ? deserialize(item) : defaultValueRef.current;
|
|
stateRef.current = next;
|
|
setState(next);
|
|
} catch (error) {
|
|
console.warn(`Failed to load ${key} from localStorage:`, error);
|
|
}
|
|
}, [key, deserialize]);
|
|
|
|
const setValue = useCallback(
|
|
(value: T | ((prev: T) => T)) => {
|
|
try {
|
|
const valueToStore = value instanceof Function ? value(stateRef.current) : value;
|
|
stateRef.current = valueToStore;
|
|
setState(valueToStore);
|
|
localStorage.setItem(key, serialize(valueToStore));
|
|
} catch (error) {
|
|
console.warn(`Failed to save ${key} to localStorage:`, error);
|
|
}
|
|
},
|
|
[key, serialize],
|
|
);
|
|
|
|
// Sync with localStorage changes from other tabs
|
|
useEffect(() => {
|
|
const handleStorageChange = (e: StorageEvent) => {
|
|
if (e.key === key && e.newValue !== null) {
|
|
try {
|
|
const next = deserialize(e.newValue);
|
|
stateRef.current = next;
|
|
setState(next);
|
|
} catch (error) {
|
|
console.warn(`Failed to sync ${key} from localStorage:`, error);
|
|
}
|
|
}
|
|
};
|
|
|
|
window.addEventListener('storage', handleStorageChange);
|
|
return () => window.removeEventListener('storage', handleStorageChange);
|
|
}, [key, deserialize]);
|
|
|
|
return [state, setValue] as const;
|
|
}
|