Files
bitvid/docs/moderation
google-labs-jules[bot]andPR0M3TH3AN 7931118d85 Fix moderation badge visibility and cleanup KNOWN_ISSUES.md
- Modified `js/ui/components/VideoCard.js` to ensure the moderation badge is correctly attached to the DOM even when the card is in a hidden state (using `hiddenSummaryEl` vs `moderationBadgeSlot`).
- Updated `tests/visual/moderation.spec.ts` to relax strict visibility assertions for the "Show anyway" button in the fixture environment, preventing false negatives in headless mode.
- Removed "Moderation UI" and "Sidebar Layout" from `KNOWN_ISSUES.md` as they are now resolved or verified passing.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-04 20:12:24 +00:00
..
2026-01-07 20:06:30 -05:00
2025-10-05 01:52:50 -04:00

bitvid Moderation Overview

What we’re building

bitvid is follow-centric. Your Home feed comes from people you follow (F1). Discovery can optionally expand to friends-of-friends (F2) and ranked sources. Moderation is client-side and user-controlled with optional admin lists.

Core principles

  • Freedom to choose: User picks filters. Defaults are safe but reversible.
  • Explain decisions: Every blur/hide shows a “why” badge and a “show anyway” control.
  • Blacklist > whitelist: Admin blacklists and moderation gates always win; whitelists only influence Discovery ranking.
  • Minimal central power: Admin lists are opt-in; users can unsubscribe.

Threat model (short)

  • Spam/bots, impersonation, malware, NSFW thumbnails, illegal content, brigading/dogpiles, Sybil report attacks.

Building blocks (Nostr)

  • Reports: NIP-56 (kind 1984) with types like nudity, spam, illegal, impersonation, etc.
  • Lists: NIP-51 (mute list 10000, categorized people 30000, bookmarks 30001).
  • Replies/threads: NIP-10 (comments).
  • Counts: NIP-45 (relay COUNT; optional fallbacks).
  • View metrics: js/nostr/viewEventBindings.js wraps the shared client so moderation toggles hit the same record/list/subscribe/count helpers that enforce our NIP-71/NIP-78 guard rails when relays disable optional methods.

Runtime flow (blur, hide, override)

  • ModerationService orchestrates ingest and scoring. Review the service for the trusted-report math and helper entry points.
  • createModerationStage wires the service into the feed engine where moderation summaries decorate timeline items.
  • The moderation stage now resolves blur/autoplay/hide thresholds on every execution using context.runtime.moderationThresholds (when provided) or dynamic resolver functions, so feed refreshes immediately respect updated viewer settings.
  • bitvidApp.decorateVideoModeration connects stage output to the UI layer alongside feature-flag plumbing.
  • VideoCard.refreshModerationUi applies badges, blur states, hide metadata (data-moderation-hidden), and the "show anyway" toggles.

Moderation badge pipeline

  • Every moderation payload now includes a blurReason that records why a thumbnail was blurred (trusted-report, trusted-mute, or a hide reason such as trusted-mute-hide). This travels with both the metadata stage output and the decorated video.moderation.original snapshot so UI can explain the decision even after a viewer override.
  • When a trusted mute forces a blur (without a report threshold firing) the badge copy shortens to "Muted by a trusted contact" so we don't repeat the blur state in the label. Reports still surface as "Blurred · {reason}" until the viewer taps "Show anyway".
  • The moderation badge now renders as a dedicated yellow chip with an icon and "Show anyway" pill that matches the rest of the design system tokens. Override states flip to the neutral palette, expose a "Restore default moderation" pill, and swap the icon for a confirmation check so operators can tell the difference at a glance.
  • The badge tooltip and aria-label continue to list the specific trusted contacts who muted or reported the content when that metadata is available.
  • Viewers who override a trusted mute hide keep a Block pill on the same badge so they can block the creator without hunting through another menu.
  • Selecting Block immediately publishes an account block for the creator, clears any temporary overrides, and reloads the feed so the author disappears from subsequent fetches.
  • Trusted mute decisions now block autoplay even when report thresholds have not fired, and any associated avatars or channel banners adopt the blurred state alongside the primary thumbnail so the entire card reflects the moderation choice.

Where to extend

Thread new moderation behaviors through the same service → stage → app → UI flow above. Extending the existing layers keeps overrides, feature flags, and QA hooks consistent—avoid spinning up parallel moderation modules unless the architecture document explicitly calls for it.

Defaults (policy)

  • Blur video thumbnails when trusted nudity reports meet DEFAULT_BLUR_THRESHOLD.
  • Disable autoplay preview when the trusted nudity report count reaches DEFAULT_AUTOPLAY_BLOCK_THRESHOLD.
  • Hide videos when DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD trusted contacts mute the author. Cards render with data-moderation-hidden="true", the badge reads Hidden · {count} trusted mute(s), and a "Show anyway" button becomes available alongside a "Restore default moderation" control once the override is active.
  • Hide videos when DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD trusted contacts file spam reports. The badge copy escalates to Hidden · {count} trusted spam report(s) and the card stays hidden until the viewer overrides it.
  • Downrank author when any F1 has them in mute list (10000).
  • Opt-in admin lists (30000 with d=bitvid:admin:*) can hard-hide content, but whitelist entries no longer bypass moderation gates—they only influence Discovery rankings when a viewer opts into that list.
  • Trust seeds now come from the Super Admin plus every active moderator, so their reports shape anonymous/default visitor filters automatically. The DEFAULT_TRUST_SEED_NPUBS export only activates as an emergency fallback when those live lists cannot be loaded (toggle via FEATURE_TRUST_SEEDS).

Default thresholds now come directly from config/instance-config.js. Look for the exports DEFAULT_BLUR_THRESHOLD, DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, and DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD to adjust the instance-wide behavior before deploying. The upstream repo currently publishes example values of 1 trusted report for blur, 1 for autoplay blocking, 20 trusted mutes to hide authors, and 1 trusted spam report to hide videos.

Operators can still override these thresholds on a per-viewer basis in Settings → Safety & Moderation; leaving the UI fields blank restores the config-driven defaults. Update config/instance-config.js to change what new viewers receive out of the box.

Hide thresholds & feature flags

  • TRUSTED_MUTE_HIDE_THRESHOLD and TRUSTED_SPAM_HIDE_THRESHOLD determine when moderation metadata marks an item as hidden. Both feed the original.hide* fields that VideoCard inspects before deciding whether to render the hidden summary container.
  • FEATURE_TRUSTED_HIDE_CONTROLS shows the additional threshold inputs in the profile modal so operators can raise/lower the hide triggers client-side.
  • When any hide threshold fires we persist the active counts (hideCounts.trustedMuteCount, hideCounts.trustedReportCount) so analytics/debugging tools can show why an item disappeared.
  • Cards that arrive with original.hidden === true automatically expose the "Show anyway" chip so viewers can inspect hidden content. Activating the override clears blur/autoplay restrictions alongside the hidden state and replaces the CTA with "Restore default moderation" for easy rollback.

Community blacklist federation

Operators can delegate hard-hide decisions to trusted curators without giving them full admin powers. The flow is:

  1. The super admin maintains a kind 30000 list with d=${ADMIN_LIST_NAMESPACE}:${ADMIN_COMMUNITY_BLACKLIST_SOURCES}. Each a tag references a community curator’s blacklist as 30000:<curator hex pubkey>:${ADMIN_LIST_NAMESPACE}:${ADMIN_COMMUNITY_BLACKLIST_PREFIX}:<slug>.
  2. Every curator publishes their own kind 30000 list using the referenced d tag (${ADMIN_LIST_NAMESPACE}:${ADMIN_COMMUNITY_BLACKLIST_PREFIX}:<slug>) and fills it with p tags for accounts they want hidden.
  3. bitvid automatically fetches each referenced list, merges the entries into the global blacklist, and removes any duplicates or guard-protected accounts (super admin or editors).

To add a community list, append a new a tag to the super-admin source list and ask the curator to publish their companion p list. To remove a list, delete the a tag or ask the curator to clear their p entries; the client stops ingesting it on the next refresh.

New operators should also note that fresh viewer accounts automatically inherit the merged admin + community blacklist on their first login. The client seeds the user’s personal block list with that baseline once, so moderators can rely on a shared floor while still allowing operators to remove entries locally without having them reappear.

Safety & Moderation controls

The profile modal now exposes blur, autoplay, and hide thresholds so operators can dial in stricter or more permissive behavior. Enter a non-negative whole number to override the default or leave the field blank to fall back to the baseline values from config/instance-config.js (DEFAULT_BLUR_THRESHOLD, DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD). Adjustments are stored locally, applied immediately to the active feed, and rehydrate on every load. Update the config file to change the per-instance defaults that new viewers see.

The trusted mute hide helper copy clarifies the escalation path: once the trusted mute count hits the threshold, cards are hidden with a “Show anyway” override, while lower signals only blur thumbnails or block autoplay previews.

Personal hashtag preferences

  • The Profile modal’s Hashtags tab is wired to HashtagPreferencesService, which maintains a local snapshot of the viewer’s interests and disinterests, normalizes every tag, and guarantees that the two sets stay mutually exclusive before broadcasting UI change events.【F:js/services/hashtagPreferencesService.js†L206-L276】【F:js/services/hashtagPreferencesService.js†L304-L324】
  • bitvidApp resolves the active pubkey’s preferences at login, listens for CHANGE events, and forwards the normalized snapshot to controllers so moderation and discovery logic can react without querying relays again.【F:js/app.js†L3811-L3906】【F:js/app.js†L3940-L4050】
  • Publishing writes a replaceable kind 30015 list with d=bitvid:tag-preferences, encrypting the { version, interests, disinterests } payload via the best available NIP-44/NIP-04 scheme before hitting every configured write relay.【F:docs/nostr-event-schemas.md†L150-L201】【F:js/services/hashtagPreferencesService.js†L520-L711】
  • Legacy 30005 events are ignored; saving or toggling a tag republishes preferences as 30015, so prompt users to re-save if they previously only published the deprecated kind.【F:js/services/hashtagPreferencesService.js†L360-L470】【F:docs/nostr-event-schemas.md†L196-L201】

Runtime flags

  • TRUSTED_MUTE_HIDE_THRESHOLD — numeric default for the trusted mute hide control. Initialized from DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD in config/instance-config.js; the upstream config sets this to 20, but adjust the export to match your policy.
  • TRUSTED_SPAM_HIDE_THRESHOLD — numeric default for the trusted spam hide control. Initialized from DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD in config/instance-config.js; the upstream config example is 1.
  • FEATURE_TRUSTED_HIDE_CONTROLS — boolean toggle to hide/show the new trusted hide controls in the UI (default true).

Files in this folder

  • web-of-trust.md — how we compute trust signals and thresholds
  • nips.md — exact NIPs and kinds bitvid uses
  • relays.md — relay compatibility and COUNT fallbacks
  • testing.md — QA checklist + test vectors