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) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gómez
2026-04-15 14:57:47 +02:00
parent c4c6e781b3
commit 31379338b5
2 changed files with 75 additions and 6 deletions

View File

@@ -148,7 +148,11 @@ export function ScrollDetailRenderer({ event }: { event: NostrEvent }) {
)}
</div>
<ScrollExecutor params={params} wasmBase64={event.content} />
<ScrollExecutor
params={params}
wasmBase64={event.content}
eventId={event.id}
/>
</div>
);
}

View File

@@ -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<string, string>;
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<string, string>;
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<string, string> = {};
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<ScrollRuntimeState>("idle");
const [paramValues, setParamValues] =
useState<Record<string, string>>(defaultValues);
useState<Record<string, string>>(initialValues);
const [displayedEventsMap, setDisplayedEventsMap] = useState<
Map<string, NostrEvent>
>(new Map());
@@ -50,10 +100,15 @@ export function ScrollExecutor({ params, wasmBase64 }: ScrollExecutorProps) {
const [activeSubs, setActiveSubs] = useState<SubscriptionInfo[]>([]);
const [eventCount, setEventCount] = useState(0);
const controllerRef = useRef<ScrollRuntimeController | null>(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();