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`.
This commit is contained in:
google-labs-jules[bot] committed 2026-01-10 22:19:54 +00:00
1 parent 760eeb906e
commit c080b83108
2 files changed
+182 -5

No files matched your search

+15
View File
@@ -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");
+167 -5
View File
@@ -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) {