From c080b83108359de28f8ac0abc75dc0c71a2cc1ea Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 22:19:54 +0000 Subject: [PATCH] feat: seed anonymous session actors with baseline block list - Added `seedLocalBaseline` to `js/userBlocks.js` to merge admin blacklist into local block list for session actors. - Implemented `_saveLocal` and `_loadLocal` in `js/userBlocks.js` to persist local-only block lists to `localStorage`. - Updated `loadBlocks` to prioritize local storage for session actors, bypassing relay fetching when local data exists. - Modified `js/app.js` to call `seedLocalBaseline` with the admin blacklist during initialization if the user is anonymous. - Ensured `UserBlockListManager` correctly resolves `localStorage` in various environments (browser/test). - Verified changes with `tests/user-blocks-local-seed.test.mjs`. --- js/app.js | 15 +++++ js/userBlocks.js | 172 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 182 insertions(+), 5 deletions(-) diff --git a/js/app.js b/js/app.js index 6095e980..922563f6 100644 --- a/js/app.js +++ b/js/app.js @@ -482,6 +482,21 @@ class Application { await Promise.all([accessControlPromise, adminPanePromise]); + if (!this.pubkey && nostrClient.sessionActor?.pubkey) { + const blacklist = accessControl.getBlacklist(); + try { + await userBlocks.seedLocalBaseline( + nostrClient.sessionActor.pubkey, + Array.from(blacklist || []), + ); + } catch (error) { + devLogger.warn( + "[app.init()] Failed to seed local baseline for session actor:", + error, + ); + } + } + // Grab the "Subscriptions" link by its id in the sidebar this.subscriptionsLink = document.getElementById("subscriptionsLink"); diff --git a/js/userBlocks.js b/js/userBlocks.js index 5d2ea62b..474cfe84 100644 --- a/js/userBlocks.js +++ b/js/userBlocks.js @@ -67,6 +67,7 @@ const BACKGROUND_BLOCKLIST_TIMEOUT_MS = 6000; const BLOCKLIST_STORAGE_PREFIX = "bitvid:user-blocks"; const BLOCKLIST_SEEDED_KEY_PREFIX = `${BLOCKLIST_STORAGE_PREFIX}:seeded:v1`; const BLOCKLIST_REMOVALS_KEY_PREFIX = `${BLOCKLIST_STORAGE_PREFIX}:removals:v1`; +const BLOCKLIST_LOCAL_KEY_PREFIX = `${BLOCKLIST_STORAGE_PREFIX}:local:v1`; function normalizeEncryptionToken(value) { if (typeof value !== "string") { @@ -434,7 +435,8 @@ function writeSeededFlag(actorHex, seeded) { return; } - if (typeof localStorage === "undefined") { + const storage = resolveStorage(); + if (!storage) { return; } @@ -539,6 +541,85 @@ function normalizeHex(pubkey) { return null; } +function resolveStorage() { + if (typeof localStorage !== "undefined") { + return localStorage; + } + if (typeof window !== "undefined" && window.localStorage) { + return window.localStorage; + } + if (typeof globalThis !== "undefined" && globalThis.localStorage) { + return globalThis.localStorage; + } + return null; +} + +function readLocalBlocks(actorHex) { + if (typeof actorHex !== "string" || !actorHex) { + return null; + } + + const storage = resolveStorage(); + if (!storage) { + return null; + } + + const key = `${BLOCKLIST_LOCAL_KEY_PREFIX}:${actorHex}`; + + try { + const raw = storage.getItem(key); + if (raw === null) { + return null; + } + + const parsed = JSON.parse(raw); + if (!Array.isArray(parsed)) { + return new Set(); + } + + const normalized = parsed + .map((entry) => + typeof entry === "string" ? entry.trim().toLowerCase() : "", + ) + .filter((entry) => /^[0-9a-f]{64}$/.test(entry)); + + return new Set(normalized); + } catch (error) { + userLogger.warn( + `[UserBlockList] Failed to read local blocks for ${actorHex}:`, + error, + ); + return null; + } +} + +function writeLocalBlocks(actorHex, blocks) { + if (typeof actorHex !== "string" || !actorHex) { + return; + } + + if (typeof localStorage === "undefined") { + return; + } + + const key = `${BLOCKLIST_LOCAL_KEY_PREFIX}:${actorHex}`; + + try { + if (!blocks || !blocks.size) { + // We persist empty sets as [] to distinguish from "not found" (null) + storage.setItem(key, "[]"); + return; + } + + storage.setItem(key, JSON.stringify(Array.from(blocks))); + } catch (error) { + userLogger.warn( + `[UserBlockList] Failed to persist local blocks for ${actorHex}:`, + error, + ); + } +} + class UserBlockListManager { constructor() { this.blockedPubkeys = new Set(); @@ -631,6 +712,19 @@ class UserBlockListManager { emitStatus({ status: "loading", relays: Array.from(nostrClient.relays || []) }); + const localBlocks = this._loadLocal(normalized); + if (localBlocks) { + this.blockedPubkeys = localBlocks; + this.loaded = true; + this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, { + action: "sync", + blockedPubkeys: Array.from(this.blockedPubkeys), + source: "local", + }); + emitStatus({ status: "settled" }); + return; + } + const applyBlockedPubkeys = (nextValues, meta = {}) => { const nextSet = new Set(Array.isArray(nextValues) ? nextValues : []); this.blockedPubkeys = nextSet; @@ -1158,6 +1252,14 @@ class UserBlockListManager { } } + _loadLocal(actorHex) { + return readLocalBlocks(actorHex); + } + + _saveLocal(actorHex) { + writeLocalBlocks(actorHex, this.blockedPubkeys); + } + _getSeedState(actorHex) { const normalized = normalizeHex(actorHex); if (!normalized) { @@ -1226,10 +1328,6 @@ class UserBlockListManager { return { ok: true, seeded: false, reason: "already-seeded" }; } - if (this.blockedPubkeys.size > 0) { - return { ok: true, seeded: false, reason: "non-empty" }; - } - const removals = state.removals; const additions = new Set(); @@ -1242,6 +1340,9 @@ class UserBlockListManager { if (candidateHex === actorHex) { continue; } + if (this.blockedPubkeys.has(candidateHex)) { + continue; + } if (removals.has(candidateHex)) { continue; } @@ -1264,6 +1365,7 @@ class UserBlockListManager { this._setSeeded(actorHex, true); additions.forEach((hex) => this._clearSeedRemoval(actorHex, hex)); + this._saveLocal(actorHex); try { this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, { @@ -1279,6 +1381,66 @@ class UserBlockListManager { return { ok: true, seeded: true, addedPubkeys: Array.from(additions) }; } + async seedLocalBaseline(userPubkey, candidateNpubs = []) { + const actorHex = normalizeHex(userPubkey); + if (!actorHex) { + return { ok: false, seeded: false, reason: "invalid-user" }; + } + + await this.ensureLoaded(actorHex); + + const state = this._getSeedState(actorHex); + if (state.seeded) { + return { ok: true, seeded: false, reason: "already-seeded" }; + } + + if (this.blockedPubkeys.size > 0) { + return { ok: true, seeded: false, reason: "non-empty" }; + } + + const removals = state.removals; + const additions = new Set(); + + const candidates = Array.isArray(candidateNpubs) ? candidateNpubs : []; + for (const candidate of candidates) { + const candidateHex = normalizeHex(candidate); + if (!candidateHex) { + continue; + } + if (candidateHex === actorHex) { + continue; + } + if (removals.has(candidateHex)) { + continue; + } + additions.add(candidateHex); + } + + if (!additions.size) { + this._setSeeded(actorHex, true); + return { ok: true, seeded: true, addedPubkeys: [] }; + } + + additions.forEach((hex) => this.blockedPubkeys.add(hex)); + + this._setSeeded(actorHex, true); + additions.forEach((hex) => this._clearSeedRemoval(actorHex, hex)); + + try { + this.emitter.emit(USER_BLOCK_EVENTS.CHANGE, { + action: "seed", + actorPubkey: actorHex, + blockedPubkeys: Array.from(this.blockedPubkeys), + addedPubkeys: Array.from(additions), + localOnly: true, + }); + } catch (error) { + userLogger.warn("[UserBlockList] Failed to emit local seed change event:", error); + } + + return { ok: true, seeded: true, addedPubkeys: Array.from(additions) }; + } + async addBlock(targetPubkey, userPubkey) { const actorHex = normalizeHex(userPubkey); if (!actorHex) {