diff --git a/config/instance-config.js b/config/instance-config.js index e2768f8e..fc88ffa9 100644 --- a/config/instance-config.js +++ b/config/instance-config.js @@ -191,6 +191,46 @@ export const DEFAULT_WHITELIST_MODE_ENABLED = true; */ export const ALLOW_NSFW_CONTENT = false; +/** + * Baseline trusted-report count that triggers thumbnail blurring. + * + * When a video receives at least this many trusted reports for the active + * category (for example, `nudity`), thumbnails render with the blurred preview + * by default. Align this value with your moderation policy — the upstream + * deployment targets 3 trusted reports before blurring. + */ +export const DEFAULT_BLUR_THRESHOLD = 3; + +/** + * Baseline trusted-report count that blocks autoplay during browsing. + * + * Videos that meet or exceed this count will not autoplay in feeds unless the + * viewer explicitly opts in. Increase the number for a more permissive stance + * or decrease it if you want autoplay to stop sooner. The upstream deployment + * targets 2 trusted reports before blocking autoplay. + */ +export const DEFAULT_AUTOPLAY_BLOCK_THRESHOLD = 2; + +/** + * Trusted mute threshold that hides creators globally. + * + * Once this many trusted accounts mute a creator, their videos disappear from + * feeds until an operator intervenes. Keep the value low (1–2) to rapidly + * enforce community standards or raise it to tolerate more reports before + * hiding. + */ +export const DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD = 1; + +/** + * Trusted spam/report threshold that hides videos globally. + * + * When a video's trusted report count reaches this number, bitvid treats it as + * spam or policy violating content and removes it from default feeds. Align + * the value with your moderation policy — 3 trusted reports is the upstream + * baseline. + */ +export const DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD = 3; + /** * Optional theme accent overrides for light and dark mode. * diff --git a/js/app.js b/js/app.js index 49b87591..825a3bde 100644 --- a/js/app.js +++ b/js/app.js @@ -19,6 +19,10 @@ import { URL_FIRST_ENABLED, getTrustedMuteHideThreshold, getTrustedSpamHideThreshold, + DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + DEFAULT_BLUR_THRESHOLD, + DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, + DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, } from "./constants.js"; import { attachHealthBadges } from "./gridHealth.js"; import { attachUrlHealthBadges } from "./urlHealthObserver.js"; @@ -6576,20 +6580,30 @@ class Application { const defaults = this.defaultModerationSettings || getDefaultModerationSettings(); const defaultBlur = Number.isFinite(defaults?.blurThreshold) ? Math.max(0, Math.floor(defaults.blurThreshold)) - : 1; + : DEFAULT_BLUR_THRESHOLD; const defaultAutoplay = Number.isFinite(defaults?.autoplayBlockThreshold) ? Math.max(0, Math.floor(defaults.autoplayBlockThreshold)) - : 1; + : DEFAULT_AUTOPLAY_BLOCK_THRESHOLD; + + const runtimeMuteSource = getTrustedMuteHideThreshold(); + const runtimeTrustedMute = Number.isFinite(runtimeMuteSource) + ? Math.max(0, Math.floor(runtimeMuteSource)) + : DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD; const defaultTrustedMuteHide = Number.isFinite( defaults?.trustedMuteHideThreshold, ) ? Math.max(0, Math.floor(defaults.trustedMuteHideThreshold)) - : getTrustedMuteHideThreshold(); + : runtimeTrustedMute; + + const runtimeSpamSource = getTrustedSpamHideThreshold(); + const runtimeTrustedSpam = Number.isFinite(runtimeSpamSource) + ? Math.max(0, Math.floor(runtimeSpamSource)) + : DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD; const defaultTrustedSpamHide = Number.isFinite( defaults?.trustedSpamHideThreshold, ) ? Math.max(0, Math.floor(defaults.trustedSpamHideThreshold)) - : getTrustedSpamHideThreshold(); + : runtimeTrustedSpam; const blurSource = Number.isFinite(settings?.blurThreshold) ? Math.max(0, Math.floor(settings.blurThreshold)) diff --git a/js/config.js b/js/config.js index 403bef54..c16aece7 100644 --- a/js/config.js +++ b/js/config.js @@ -20,6 +20,10 @@ import { ADMIN_WHITELIST_MODE_STORAGE_KEY, DEFAULT_WHITELIST_MODE_ENABLED, ALLOW_NSFW_CONTENT, + DEFAULT_BLUR_THRESHOLD, + DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, + DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, WATCH_HISTORY_KIND, WATCH_HISTORY_LIST_IDENTIFIER, WATCH_HISTORY_LEGACY_LIST_IDENTIFIERS, @@ -69,6 +73,10 @@ export const ADMIN_LIST_NAMESPACE = "bitvid:admin"; // Reserved for Nostr lists export const ADMIN_COMMUNITY_BLACKLIST_SOURCES = "community:blacklist:sources"; export const ADMIN_COMMUNITY_BLACKLIST_PREFIX = "community:blacklist"; export { ALLOW_NSFW_CONTENT }; +export { DEFAULT_BLUR_THRESHOLD }; +export { DEFAULT_AUTOPLAY_BLOCK_THRESHOLD }; +export { DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD }; +export { DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD }; const WHITELIST_MODE_KEY = ADMIN_WHITELIST_MODE_STORAGE_KEY; const DEFAULT_WHITELIST_ENABLED = DEFAULT_WHITELIST_MODE_ENABLED; diff --git a/js/constants.js b/js/constants.js index 0171cee2..0e09dc66 100644 --- a/js/constants.js +++ b/js/constants.js @@ -1,9 +1,120 @@ +import { + DEFAULT_AUTOPLAY_BLOCK_THRESHOLD as CONFIG_DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + DEFAULT_BLUR_THRESHOLD as CONFIG_DEFAULT_BLUR_THRESHOLD, + DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD as CONFIG_DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, + DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD as CONFIG_DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, +} from "./config.js"; + export const DEFAULT_TRUST_SEED_NPUBS = Object.freeze([ // Rollout: trust seeds count as F1 contacts. Rollback: flip FEATURE_TRUST_SEEDS to false. "npub1424242424242424242424242424242424242424242424242424qamrcaj", "npub1hwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwamhwasxw04hu", ]); +function coerceNonNegativeInteger(value, fallback) { + const numeric = Number(value); + if (!Number.isFinite(numeric)) { + return fallback; + } + + const sanitized = Math.max(0, Math.floor(numeric)); + if (!Number.isFinite(sanitized)) { + return fallback; + } + + return sanitized; +} + +function coerceBoolean(value, fallback) { + if (typeof value === "boolean") { + return value; + } + if (typeof value === "string") { + const normalized = value.trim().toLowerCase(); + if (!normalized) { + return fallback; + } + if (["false", "0", "off", "no"].includes(normalized)) { + return false; + } + if (["true", "1", "on", "yes"].includes(normalized)) { + return true; + } + } + if (value == null) { + return fallback; + } + return Boolean(value); +} + +function freezeTrackers(list) { + return Object.freeze([...list]); +} + +function sanitizeTrackerList(candidate, fallbackTrackers) { + const fallback = Array.isArray(fallbackTrackers) ? fallbackTrackers : []; + const input = Array.isArray(candidate) ? candidate : fallback; + const seen = new Set(); + const sanitized = []; + + for (const tracker of input) { + if (typeof tracker !== "string") { + continue; + } + const trimmed = tracker.trim(); + if (!trimmed) { + continue; + } + if (!/^wss:\/\//i.test(trimmed)) { + continue; + } + const normalized = trimmed.toLowerCase(); + if (seen.has(normalized)) { + continue; + } + seen.add(normalized); + sanitized.push(trimmed); + } + + if (!sanitized.length) { + return [...fallback]; + } + + return sanitized; +} + +const DEFAULT_WSS_TRACKERS = Object.freeze([ + "wss://tracker.openwebtorrent.com", + "wss://tracker.fastcast.nz", + "wss://tracker.webtorrent.dev", + "wss://tracker.sloppyta.co:443/announce", +]); + +const SANITIZED_DEFAULT_BLUR_THRESHOLD = coerceNonNegativeInteger( + CONFIG_DEFAULT_BLUR_THRESHOLD, + 1, +); +const SANITIZED_DEFAULT_AUTOPLAY_BLOCK_THRESHOLD = coerceNonNegativeInteger( + CONFIG_DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + 1, +); +const SANITIZED_DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD = coerceNonNegativeInteger( + CONFIG_DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, + 1, +); +const SANITIZED_DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD = coerceNonNegativeInteger( + CONFIG_DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, + 3, +); + +export const DEFAULT_BLUR_THRESHOLD = SANITIZED_DEFAULT_BLUR_THRESHOLD; +export const DEFAULT_AUTOPLAY_BLOCK_THRESHOLD = + SANITIZED_DEFAULT_AUTOPLAY_BLOCK_THRESHOLD; +export const DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD = + SANITIZED_DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD; +export const DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD = + SANITIZED_DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD; + const DEFAULT_FLAGS = Object.freeze({ URL_FIRST_ENABLED: true, // try URL before magnet in the player ACCEPT_LEGACY_V1: true, // accept v1 magnet-only notes @@ -12,14 +123,9 @@ const DEFAULT_FLAGS = Object.freeze({ FEATURE_PUBLISH_NIP71: false, FEATURE_TRUST_SEEDS: true, // Rollback: disable to drop baseline trust seeds without code changes. FEATURE_TRUSTED_HIDE_CONTROLS: true, - TRUSTED_MUTE_HIDE_THRESHOLD: 1, - TRUSTED_SPAM_HIDE_THRESHOLD: 3, - WSS_TRACKERS: Object.freeze([ - "wss://tracker.openwebtorrent.com", - "wss://tracker.fastcast.nz", - "wss://tracker.webtorrent.dev", - "wss://tracker.sloppyta.co:443/announce", - ]), + TRUSTED_MUTE_HIDE_THRESHOLD: DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, + TRUSTED_SPAM_HIDE_THRESHOLD: DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, + WSS_TRACKERS: DEFAULT_WSS_TRACKERS, }); const globalScope = typeof globalThis === "object" && globalThis ? globalThis : undefined; @@ -47,77 +153,6 @@ const runtimeFlags = (() => { return initial; })(); -function coerceBoolean(value, fallback) { - if (typeof value === "boolean") { - return value; - } - if (typeof value === "string") { - const normalized = value.trim().toLowerCase(); - if (!normalized) { - return fallback; - } - if (["false", "0", "off", "no"].includes(normalized)) { - return false; - } - if (["true", "1", "on", "yes"].includes(normalized)) { - return true; - } - } - if (value == null) { - return fallback; - } - return Boolean(value); -} - -function sanitizeTrackerList(candidate) { - const input = Array.isArray(candidate) ? candidate : DEFAULT_FLAGS.WSS_TRACKERS; - const seen = new Set(); - const sanitized = []; - - for (const tracker of input) { - if (typeof tracker !== "string") { - continue; - } - const trimmed = tracker.trim(); - if (!trimmed) { - continue; - } - if (!/^wss:\/\//i.test(trimmed)) { - continue; - } - const normalized = trimmed.toLowerCase(); - if (seen.has(normalized)) { - continue; - } - seen.add(normalized); - sanitized.push(trimmed); - } - - if (!sanitized.length) { - return [...DEFAULT_FLAGS.WSS_TRACKERS]; - } - - return sanitized; -} - -function freezeTrackers(list) { - return Object.freeze([...list]); -} - -function coerceNonNegativeInteger(value, fallback) { - const numeric = Number(value); - if (!Number.isFinite(numeric)) { - return fallback; - } - - const sanitized = Math.max(0, Math.floor(numeric)); - if (!Number.isFinite(sanitized)) { - return fallback; - } - - return sanitized; -} - export let URL_FIRST_ENABLED = coerceBoolean( runtimeFlags.URL_FIRST_ENABLED, DEFAULT_FLAGS.URL_FIRST_ENABLED @@ -154,7 +189,7 @@ export let FEATURE_TRUSTED_HIDE_CONTROLS = coerceBoolean( ); export let WSS_TRACKERS = freezeTrackers( - sanitizeTrackerList(runtimeFlags.WSS_TRACKERS) + sanitizeTrackerList(runtimeFlags.WSS_TRACKERS, DEFAULT_FLAGS.WSS_TRACKERS) ); export let TRUSTED_MUTE_HIDE_THRESHOLD = coerceNonNegativeInteger( @@ -167,12 +202,6 @@ export let TRUSTED_SPAM_HIDE_THRESHOLD = coerceNonNegativeInteger( DEFAULT_FLAGS.TRUSTED_SPAM_HIDE_THRESHOLD ); -export const DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD = - DEFAULT_FLAGS.TRUSTED_MUTE_HIDE_THRESHOLD; - -export const DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD = - DEFAULT_FLAGS.TRUSTED_SPAM_HIDE_THRESHOLD; - Object.defineProperty(runtimeFlags, "URL_FIRST_ENABLED", { configurable: true, enumerable: true, @@ -272,7 +301,9 @@ Object.defineProperty(runtimeFlags, "WSS_TRACKERS", { return [...WSS_TRACKERS]; }, set(next) { - WSS_TRACKERS = freezeTrackers(sanitizeTrackerList(next)); + WSS_TRACKERS = freezeTrackers( + sanitizeTrackerList(next, DEFAULT_FLAGS.WSS_TRACKERS) + ); }, }); diff --git a/js/feedEngine/stages.js b/js/feedEngine/stages.js index 4b7bdc5c..ec31406b 100644 --- a/js/feedEngine/stages.js +++ b/js/feedEngine/stages.js @@ -2,6 +2,10 @@ import { getApplication } from "../applicationContext.js"; import { IS_DEV_MODE } from "../config.js"; +import { + DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + DEFAULT_BLUR_THRESHOLD, +} from "../constants.js"; import nostrService from "../services/nostrService.js"; import moderationService from "../services/moderationService.js"; import logger from "../utils/logger.js"; @@ -196,8 +200,8 @@ export function createWatchHistorySuppressionStage({ export function createModerationStage({ stageName = "moderation", - autoplayThreshold = 2, - blurThreshold = 3, + autoplayThreshold = DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + blurThreshold = DEFAULT_BLUR_THRESHOLD, reportType = "nudity", service = null, getService, @@ -213,10 +217,10 @@ export function createModerationStage({ const normalizedAutoplayThreshold = Number.isFinite(autoplayThreshold) ? Math.max(0, Math.floor(autoplayThreshold)) - : 2; + : DEFAULT_AUTOPLAY_BLOCK_THRESHOLD; const normalizedBlurThreshold = Number.isFinite(blurThreshold) ? Math.max(0, Math.floor(blurThreshold)) - : 3; + : DEFAULT_BLUR_THRESHOLD; const normalizedMuteHideThreshold = sanitizeThreshold( trustedMuteHideThreshold, Number.POSITIVE_INFINITY, diff --git a/js/state/cache.js b/js/state/cache.js index 22c46b7d..a743e950 100644 --- a/js/state/cache.js +++ b/js/state/cache.js @@ -1,4 +1,6 @@ import { + DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + DEFAULT_BLUR_THRESHOLD, DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, getTrustedMuteHideThreshold, @@ -33,41 +35,66 @@ const MODERATION_SETTINGS_STORAGE_KEY = "bitvid:moderationSettings:v1"; const MODERATION_SETTINGS_STORAGE_VERSION = 2; function computeDefaultModerationSettings() { - let runtimeMuteHide = DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD; - let runtimeSpamHide = DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD; + const defaultBlur = sanitizeModerationThreshold( + DEFAULT_BLUR_THRESHOLD, + DEFAULT_BLUR_THRESHOLD, + ); + const defaultAutoplay = sanitizeModerationThreshold( + DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + ); + const defaultTrustedMute = sanitizeModerationThreshold( + DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, + DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, + ); + const defaultTrustedSpam = sanitizeModerationThreshold( + DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, + DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, + ); + + let runtimeMuteHide = defaultTrustedMute; + let runtimeSpamHide = defaultTrustedSpam; try { if (typeof getTrustedMuteHideThreshold === "function") { - runtimeMuteHide = getTrustedMuteHideThreshold(); + runtimeMuteHide = sanitizeModerationThreshold( + getTrustedMuteHideThreshold(), + defaultTrustedMute, + ); } } catch (error) { userLogger.warn( "[cache.computeDefaultModerationSettings] Failed to read runtime mute hide threshold:", error, ); + runtimeMuteHide = defaultTrustedMute; } try { if (typeof getTrustedSpamHideThreshold === "function") { - runtimeSpamHide = getTrustedSpamHideThreshold(); + runtimeSpamHide = sanitizeModerationThreshold( + getTrustedSpamHideThreshold(), + defaultTrustedSpam, + ); } } catch (error) { userLogger.warn( "[cache.computeDefaultModerationSettings] Failed to read runtime spam hide threshold:", error, ); + runtimeSpamHide = defaultTrustedSpam; } return { - blurThreshold: 1, - autoplayBlockThreshold: 1, + blurThreshold: defaultBlur, + autoplayBlockThreshold: defaultAutoplay, trustedMuteHideThreshold: sanitizeModerationThreshold( runtimeMuteHide, - DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, + defaultTrustedMute, ), trustedSpamHideThreshold: sanitizeModerationThreshold( runtimeSpamHide, - DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, + defaultTrustedSpam, ), }; } diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js index 83e3488f..1d396f94 100644 --- a/js/ui/profileModalController.js +++ b/js/ui/profileModalController.js @@ -7,8 +7,10 @@ import { } from "../config.js"; import { normalizeDesignSystemContext } from "../designSystem.js"; import { - getTrustedMuteHideThreshold, - getTrustedSpamHideThreshold, + DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + DEFAULT_BLUR_THRESHOLD, + DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, + DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, RUNTIME_FLAGS, } from "../constants.js"; import { getProviderMetadata } from "../services/authProviders/index.js"; @@ -65,10 +67,10 @@ function createInternalDefaultNwcSettings() { } const DEFAULT_INTERNAL_MODERATION_SETTINGS = Object.freeze({ - blurThreshold: 1, - autoplayBlockThreshold: 1, - trustedMuteHideThreshold: getTrustedMuteHideThreshold(), - trustedSpamHideThreshold: getTrustedSpamHideThreshold(), + blurThreshold: DEFAULT_BLUR_THRESHOLD, + autoplayBlockThreshold: DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + trustedMuteHideThreshold: DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, + trustedSpamHideThreshold: DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, }); function createInternalDefaultModerationSettings() { diff --git a/tests/moderation-stage.test.mjs b/tests/moderation-stage.test.mjs index 1b0cbfe9..d06c2138 100644 --- a/tests/moderation-stage.test.mjs +++ b/tests/moderation-stage.test.mjs @@ -1,6 +1,10 @@ import test from "node:test"; import assert from "node:assert/strict"; import { createModerationStage } from "../js/feedEngine/stages.js"; +import { + DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + DEFAULT_BLUR_THRESHOLD, +} from "../js/constants.js"; test("moderation stage enforces admin lists without whitelist bypass", async () => { const whitelistedHex = "1".repeat(64); @@ -55,8 +59,8 @@ test("moderation stage enforces admin lists without whitelist bypass", async () const stage = createModerationStage({ service, reportType: "nudity", - autoplayThreshold: 1, - blurThreshold: 1, + autoplayThreshold: DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + blurThreshold: DEFAULT_BLUR_THRESHOLD, }); const items = [ diff --git a/tests/ui/profile-modal-moderation-settings.test.mjs b/tests/ui/profile-modal-moderation-settings.test.mjs index b59f99ab..de1ad73d 100644 --- a/tests/ui/profile-modal-moderation-settings.test.mjs +++ b/tests/ui/profile-modal-moderation-settings.test.mjs @@ -3,6 +3,17 @@ import assert from "node:assert/strict"; import { JSDOM } from "jsdom"; import ProfileModalController from "../../js/ui/profileModalController.js"; +import { + DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + DEFAULT_BLUR_THRESHOLD, + DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, + DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, +} from "../../js/constants.js"; + +const DEFAULT_BLUR_STRING = String(DEFAULT_BLUR_THRESHOLD); +const DEFAULT_AUTOPLAY_STRING = String(DEFAULT_AUTOPLAY_BLOCK_THRESHOLD); +const DEFAULT_TRUSTED_MUTE_STRING = String(DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD); +const DEFAULT_TRUSTED_SPAM_STRING = String(DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD); const TEMPLATE_HTML = `
@@ -67,18 +78,18 @@ beforeEach(async () => { }; storedSettings = { - blurThreshold: 1, - autoplayBlockThreshold: 1, - trustedMuteHideThreshold: 1, - trustedSpamHideThreshold: 3, + blurThreshold: DEFAULT_BLUR_THRESHOLD, + autoplayBlockThreshold: DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + trustedMuteHideThreshold: DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, + trustedSpamHideThreshold: DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, }; moderationService = { getDefaultModerationSettings() { return { - blurThreshold: 1, - autoplayBlockThreshold: 1, - trustedMuteHideThreshold: 1, - trustedSpamHideThreshold: 3, + blurThreshold: DEFAULT_BLUR_THRESHOLD, + autoplayBlockThreshold: DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + trustedMuteHideThreshold: DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, + trustedSpamHideThreshold: DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, }; }, getActiveModerationSettings() { @@ -87,7 +98,8 @@ beforeEach(async () => { updateModerationSettings(partial = {}) { if (Object.prototype.hasOwnProperty.call(partial, "blurThreshold")) { const value = partial.blurThreshold; - storedSettings.blurThreshold = value === null ? 1 : value; + storedSettings.blurThreshold = + value === null ? DEFAULT_BLUR_THRESHOLD : value; } if ( Object.prototype.hasOwnProperty.call( @@ -96,7 +108,8 @@ beforeEach(async () => { ) ) { const value = partial.autoplayBlockThreshold; - storedSettings.autoplayBlockThreshold = value === null ? 1 : value; + storedSettings.autoplayBlockThreshold = + value === null ? DEFAULT_AUTOPLAY_BLOCK_THRESHOLD : value; } if ( Object.prototype.hasOwnProperty.call( @@ -105,7 +118,8 @@ beforeEach(async () => { ) ) { const value = partial.trustedMuteHideThreshold; - storedSettings.trustedMuteHideThreshold = value === null ? 1 : value; + storedSettings.trustedMuteHideThreshold = + value === null ? DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD : value; } if ( Object.prototype.hasOwnProperty.call( @@ -114,16 +128,17 @@ beforeEach(async () => { ) ) { const value = partial.trustedSpamHideThreshold; - storedSettings.trustedSpamHideThreshold = value === null ? 3 : value; + storedSettings.trustedSpamHideThreshold = + value === null ? DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD : value; } return { ...storedSettings }; }, resetModerationSettings() { storedSettings = { - blurThreshold: 1, - autoplayBlockThreshold: 1, - trustedMuteHideThreshold: 1, - trustedSpamHideThreshold: 3, + blurThreshold: DEFAULT_BLUR_THRESHOLD, + autoplayBlockThreshold: DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + trustedMuteHideThreshold: DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, + trustedSpamHideThreshold: DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, }; return { ...storedSettings }; }, @@ -185,10 +200,10 @@ afterEach(() => { test("moderation settings save updates service and disables control", async () => { await controller.load(); - assert.equal(controller.moderationBlurInput.value, "1"); - assert.equal(controller.moderationAutoplayInput.value, "1"); - assert.equal(controller.moderationMuteHideInput.value, "1"); - assert.equal(controller.moderationSpamHideInput.value, "3"); + assert.equal(controller.moderationBlurInput.value, DEFAULT_BLUR_STRING); + assert.equal(controller.moderationAutoplayInput.value, DEFAULT_AUTOPLAY_STRING); + assert.equal(controller.moderationMuteHideInput.value, DEFAULT_TRUSTED_MUTE_STRING); + assert.equal(controller.moderationSpamHideInput.value, DEFAULT_TRUSTED_SPAM_STRING); assert.equal(controller.moderationSaveButton.disabled, true); controller.moderationBlurInput.value = "5"; @@ -249,15 +264,15 @@ test("moderation reset restores defaults and clearing inputs uses defaults", asy const resetContext = await controller.handleModerationSettingsReset(); assert.equal(resetContext.success, true); assert.deepEqual(storedSettings, { - blurThreshold: 1, - autoplayBlockThreshold: 1, - trustedMuteHideThreshold: 1, - trustedSpamHideThreshold: 3, + blurThreshold: DEFAULT_BLUR_THRESHOLD, + autoplayBlockThreshold: DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + trustedMuteHideThreshold: DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, + trustedSpamHideThreshold: DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, }); - assert.equal(controller.moderationBlurInput.value, "1"); - assert.equal(controller.moderationAutoplayInput.value, "1"); - assert.equal(controller.moderationMuteHideInput.value, "1"); - assert.equal(controller.moderationSpamHideInput.value, "3"); + assert.equal(controller.moderationBlurInput.value, DEFAULT_BLUR_STRING); + assert.equal(controller.moderationAutoplayInput.value, DEFAULT_AUTOPLAY_STRING); + assert.equal(controller.moderationMuteHideInput.value, DEFAULT_TRUSTED_MUTE_STRING); + assert.equal(controller.moderationSpamHideInput.value, DEFAULT_TRUSTED_SPAM_STRING); assert.equal( controller.moderationStatusText.textContent, "Moderation defaults restored.", @@ -276,8 +291,8 @@ test("moderation reset restores defaults and clearing inputs uses defaults", asy assert.equal(saveResult.success, true); assert.deepEqual(storedSettings, { blurThreshold: 7, - autoplayBlockThreshold: 1, - trustedMuteHideThreshold: 1, + autoplayBlockThreshold: DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + trustedMuteHideThreshold: DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, trustedSpamHideThreshold: 8, }); @@ -302,9 +317,39 @@ test("moderation reset restores defaults and clearing inputs uses defaults", asy const revertContext = await controller.handleModerationSettingsSave(); assert.equal(revertContext.success, true); assert.deepEqual(storedSettings, { - blurThreshold: 1, - autoplayBlockThreshold: 1, - trustedMuteHideThreshold: 1, - trustedSpamHideThreshold: 3, + blurThreshold: DEFAULT_BLUR_THRESHOLD, + autoplayBlockThreshold: DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + trustedMuteHideThreshold: DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, + trustedSpamHideThreshold: DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, + }); +}); + +test("guest fallback uses config moderation defaults", async () => { + moderationService.getDefaultModerationSettings = () => null; + moderationService.getActiveModerationSettings = () => null; + storedSettings = null; + + await controller.load(); + + assert.equal(controller.moderationBlurInput.value, DEFAULT_BLUR_STRING); + assert.equal( + controller.moderationAutoplayInput.value, + DEFAULT_AUTOPLAY_STRING, + ); + assert.equal( + controller.moderationMuteHideInput.value, + DEFAULT_TRUSTED_MUTE_STRING, + ); + assert.equal( + controller.moderationSpamHideInput.value, + DEFAULT_TRUSTED_SPAM_STRING, + ); + + const defaults = controller.getModerationSettingsDefaults(); + assert.deepEqual(defaults, { + blurThreshold: DEFAULT_BLUR_THRESHOLD, + autoplayBlockThreshold: DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, + trustedMuteHideThreshold: DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, + trustedSpamHideThreshold: DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD, }); });