From 31379338b506688fff2abfd268234218857b1763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Wed, 15 Apr 2026 14:57:47 +0200 Subject: [PATCH] fix: persist NIP-5C scroll parameter values and encoding settings Store scroll settings (param values, endianness, presence bytes) in localStorage keyed by event ID. Loads persisted values on mount, saves on change (skipping initial mount to avoid unnecessary writes). Filters stale param keys when scroll parameters change between versions. Closes #266 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/nostr/kinds/ScrollRenderer.tsx | 6 +- src/components/scroll/ScrollExecutor.tsx | 75 +++++++++++++++++-- 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/src/components/nostr/kinds/ScrollRenderer.tsx b/src/components/nostr/kinds/ScrollRenderer.tsx index ac2bb7f..0db3034 100644 --- a/src/components/nostr/kinds/ScrollRenderer.tsx +++ b/src/components/nostr/kinds/ScrollRenderer.tsx @@ -148,7 +148,11 @@ export function ScrollDetailRenderer({ event }: { event: NostrEvent }) { )} - + ); } diff --git a/src/components/scroll/ScrollExecutor.tsx b/src/components/scroll/ScrollExecutor.tsx index 6c39a12..d35b46d 100644 --- a/src/components/scroll/ScrollExecutor.tsx +++ b/src/components/scroll/ScrollExecutor.tsx @@ -21,9 +21,48 @@ interface ScrollExecutorProps { params: ScrollParam[]; /** Base64-encoded WASM binary */ wasmBase64: string; + /** Event ID used as localStorage key for persisting settings */ + eventId?: string; } -export function ScrollExecutor({ params, wasmBase64 }: ScrollExecutorProps) { +const SCROLL_STORAGE_PREFIX = "scroll_settings_"; + +function loadScrollSettings(eventId: string): { + paramValues?: Record; + endianness?: "LE" | "BE"; + presenceBytes?: boolean; +} { + try { + const stored = localStorage.getItem(SCROLL_STORAGE_PREFIX + eventId); + return stored ? JSON.parse(stored) : {}; + } catch { + return {}; + } +} + +function saveScrollSettings( + eventId: string, + settings: { + paramValues: Record; + endianness: "LE" | "BE"; + presenceBytes: boolean; + }, +) { + try { + localStorage.setItem( + SCROLL_STORAGE_PREFIX + eventId, + JSON.stringify(settings), + ); + } catch { + // localStorage full or unavailable — silently ignore + } +} + +export function ScrollExecutor({ + params, + wasmBase64, + eventId, +}: ScrollExecutorProps) { const { pubkey } = useAccount(); const { relays: relayStates } = useRelayState(); @@ -31,17 +70,28 @@ export function ScrollExecutor({ params, wasmBase64 }: ScrollExecutorProps) { .filter(([, state]) => state.connectionState === "connected") .map(([url]) => url); - // Pre-fill "me" params with logged-in pubkey + // Load persisted settings + const stored = eventId ? loadScrollSettings(eventId) : {}; + + // Pre-fill "me" params with logged-in pubkey, then overlay persisted values const defaultValues: Record = {}; for (const p of params) { if (p.name === "me" && p.type === "public_key" && pubkey) { defaultValues[p.name] = pubkey; } } + // Filter stored values to only include current params (remove stale keys) + const validParamNames = new Set(params.map((p) => p.name)); + const filteredStored = Object.fromEntries( + Object.entries(stored.paramValues || {}).filter(([k]) => + validParamNames.has(k), + ), + ); + const initialValues = { ...defaultValues, ...filteredStored }; const [runtimeState, setRuntimeState] = useState("idle"); const [paramValues, setParamValues] = - useState>(defaultValues); + useState>(initialValues); const [displayedEventsMap, setDisplayedEventsMap] = useState< Map >(new Map()); @@ -50,10 +100,15 @@ export function ScrollExecutor({ params, wasmBase64 }: ScrollExecutorProps) { const [activeSubs, setActiveSubs] = useState([]); const [eventCount, setEventCount] = useState(0); const controllerRef = useRef(null); + const isInitialMount = useRef(true); // Encoding options - const [endianness, setEndianness] = useState<"LE" | "BE">("BE"); - const [presenceBytes, setPresenceBytes] = useState(false); + const [endianness, setEndianness] = useState<"LE" | "BE">( + stored.endianness || "BE", + ); + const [presenceBytes, setPresenceBytes] = useState( + stored.presenceBytes ?? false, + ); const isActive = runtimeState === "loading" || runtimeState === "running"; @@ -70,6 +125,16 @@ export function ScrollExecutor({ params, wasmBase64 }: ScrollExecutorProps) { (p) => p.required && !paramValues[p.name]?.trim(), ); + // Persist settings to localStorage when they change (skip initial mount) + useEffect(() => { + if (!eventId) return; + if (isInitialMount.current) { + isInitialMount.current = false; + return; + } + saveScrollSettings(eventId, { paramValues, endianness, presenceBytes }); + }, [eventId, paramValues, endianness, presenceBytes]); + useEffect(() => { return () => { controllerRef.current?.stop();