[moderation] #25: per-video admin block list (hide a single video, not the author)

Admin moderation was author/community-level only. Admins (any editor — same gate as
the author blacklist) can now hide a SINGLE offending video.

- Record: a published kind-30000 NIP-51 list under d-tag bitvid:admin:event-blacklist,
  carrying blocked event ids as `e` tags (NOTE_TYPES.ADMIN_EVENT_BLACKLIST schema).
  Loaded/persisted through the existing admin-list machinery (adminListStore) and cached
  like whitelist/blacklist.
- accessControl: eventBlacklist Set + getEventBlacklist/isEventBlacklisted/
  addToEventBlacklist/removeFromEventBlacklist (editor-gated; normalizes hex or
  nevent/note) + onEventBlacklistChange.
- Render filter already existed (app.blacklistedEventIds, honored by every feed/grid/
  search/playback path) but was fed only by the static ADMIN_INITIAL_EVENT_BLACKLIST
  config. app._rebuildBlacklistedEventIds() now merges that static base with the dynamic
  admin list, and an onEventBlacklistChange listener rebuilds + refreshes grids on change.
- UI: a "Block this video" item in the per-video ⋯ menu (editor-only), handled by
  handleBlacklistEventAction (js/ui/moreMenu/blacklistEventAction.js).

Helpers extracted to js/adminEventBlacklistHelpers.js to keep adminListStore /
accessControl / moreMenuController under their size caps. Tests:
tests/admin-event-blacklist.test.mjs (9). Build + lint clean; admin/access/moderation
suites 56/56.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-30 15:19:34 -04:00
1 parent 2e7e2016d3
commit 3e57ab2ea9
11 files changed
+529 -44

No files matched your search

+11 -2
View File
@@ -1209,7 +1209,10 @@ export default class ApplicationBootstrap {
});
app.appChromeController.initialize();
app.blacklistedEventIds = new Set();
// Static, build-time per-event block list (operator config). Kept separate from
// the dynamic admin list (#25) so app._rebuildBlacklistedEventIds() can re-merge
// both whenever the published admin event-blacklist changes.
app._staticBlacklistedEventIds = new Set();
if (this.window?.NostrTools?.nip19?.decode) {
for (const neventStr of ADMIN_INITIAL_EVENT_BLACKLIST) {
if (!neventStr || neventStr.trim().length < 8) {
@@ -1218,7 +1221,7 @@ export default class ApplicationBootstrap {
try {
const decoded = this.window.NostrTools.nip19.decode(neventStr);
if (decoded?.type === "nevent" && decoded.data?.id) {
app.blacklistedEventIds.add(decoded.data.id);
app._staticBlacklistedEventIds.add(decoded.data.id);
}
} catch (err) {
devLogger.error(
@@ -1229,6 +1232,12 @@ export default class ApplicationBootstrap {
}
}
}
app.blacklistedEventIds = new Set(app._staticBlacklistedEventIds);
if (typeof app._rebuildBlacklistedEventIds === "function") {
// Fold in any already-loaded admin event-blacklist (accessControl may have
// hydrated from cache synchronously before this point).
app._rebuildBlacklistedEventIds();
}
app.unsubscribeFromPubkeyState = null;
app.unsubscribeFromCurrentUserState = null;