diff --git a/components/profile-modal.html b/components/profile-modal.html
index 2a4249a5..00375790 100644
--- a/components/profile-modal.html
+++ b/components/profile-modal.html
@@ -1612,6 +1612,56 @@
data-orientation="vertical"
>
+
+
+
+ Trusted creators
+
+
+ Turn off web-of-trust warnings for a creator across all
+ their videos. They stay listed here until you reset them.
+
+
+
+
+
+
+
+
+ No trusted creators yet.
+
+
+
diff --git a/js/app.js b/js/app.js
index 18ac9cf6..3e840d46 100644
--- a/js/app.js
+++ b/js/app.js
@@ -177,6 +177,10 @@ import {
setModerationOverride,
clearModerationOverride,
loadModerationOverridesFromStorage,
+ getAuthorModerationOverridesList,
+ setAuthorModerationOverride,
+ clearAuthorModerationOverride,
+ loadAuthorModerationOverridesFromStorage,
getModerationSettings,
getDefaultModerationSettings,
setModerationSettings,
@@ -918,6 +922,7 @@ class Application {
}
loadModerationOverridesFromStorage();
+ loadAuthorModerationOverridesFromStorage();
loadModerationSettingsFromStorage();
loadDmPrivacySettingsFromStorage();
this.moderationSettings = this.normalizeModerationSettings(
diff --git a/js/services/moderationDecorator.js b/js/services/moderationDecorator.js
index 2236462f..5288b22b 100644
--- a/js/services/moderationDecorator.js
+++ b/js/services/moderationDecorator.js
@@ -2,7 +2,7 @@
import { getTrustedMuteHideThreshold, getTrustedSpamHideThreshold, DEFAULT_BLUR_THRESHOLD, DEFAULT_AUTOPLAY_BLOCK_THRESHOLD, DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD } from "../constants.js";
import { formatShortNpub } from "../utils/formatters.js";
-import { getModerationOverride, getModerationSettings, getDefaultModerationSettings } from "../state/cache.js";
+import { getModerationOverride, getAuthorModerationOverride, getModerationSettings, getDefaultModerationSettings } from "../state/cache.js";
import { userLogger } from "../utils/logger.js";
/**
@@ -403,14 +403,22 @@ export class ModerationDecorator {
const computedHidden = hideTriggered && !hideBypass;
+ const authorPubkey = video.pubkey || video.author?.pubkey || "";
const overrideEntry = getModerationOverride({
eventId: video.id,
- authorPubkey: video.pubkey || video.author?.pubkey || "",
+ authorPubkey,
});
- const overrideActive = overrideEntry?.showAnyway === true;
+ // Account-level ("trusted creator") override: suppresses the WoT warning for
+ // ALL of this author's videos, exactly like a per-video "show anyway".
+ const authorOverrideEntry = getAuthorModerationOverride(authorPubkey);
+ const overrideActive =
+ overrideEntry?.showAnyway === true ||
+ authorOverrideEntry?.showAnyway === true;
const overrideUpdatedAt = Number.isFinite(overrideEntry?.updatedAt)
? Math.floor(overrideEntry.updatedAt)
- : Date.now();
+ : Number.isFinite(authorOverrideEntry?.updatedAt)
+ ? Math.floor(authorOverrideEntry.updatedAt)
+ : Date.now();
const originalHideCounts = hideCounts
? {
diff --git a/js/state/cache.js b/js/state/cache.js
index 2fca6225..9bbebca4 100644
--- a/js/state/cache.js
+++ b/js/state/cache.js
@@ -33,6 +33,12 @@ const URL_PROBE_TIMEOUT_RETRY_MS = 15 * 1000; // 15 seconds
const MODERATION_OVERRIDE_STORAGE_KEY = "bitvid:moderationOverrides:v2";
const MODERATION_OVERRIDE_STORAGE_VERSION = 2;
+// Account-level ("trusted creator") web-of-trust overrides, keyed by author hex.
+// A showAnyway entry suppresses the WoT blur/hide/warning for ALL of that
+// author's videos until the viewer resets it in the Safety tab.
+const AUTHOR_MODERATION_OVERRIDE_STORAGE_KEY =
+ "bitvid:moderationAuthorOverrides:v1";
+const AUTHOR_MODERATION_OVERRIDE_STORAGE_VERSION = 1;
const MODERATION_SETTINGS_STORAGE_KEY = "bitvid:moderationSettings:v1";
const MODERATION_SETTINGS_STORAGE_VERSION = 3;
const DM_PRIVACY_SETTINGS_STORAGE_KEY = "bitvid:dmPrivacySettings:v1";
@@ -198,6 +204,7 @@ const profileCache = new Map();
const urlHealthCache = new Map();
const urlHealthInFlight = new Map();
const moderationOverrides = new Map();
+const authorModerationOverrides = new Map();
function hasSavedProfilesChanged(previousProfiles, nextProfiles) {
if (previousProfiles === nextProfiles) {
@@ -1720,6 +1727,169 @@ export function clearModerationOverride(
return removed;
}
+// ---- Account-level ("trusted creator") web-of-trust overrides ---------------
+// Keyed by author hex; a showAnyway entry suppresses the WoT blur/hide/warning
+// for EVERY video by that author until reset. Mirrors the per-video store above.
+
+function normalizeAuthorOverrideHex(pubkey) {
+ const hex = normalizeHexPubkey(pubkey);
+ return hex ? hex.toLowerCase() : "";
+}
+
+export function getAuthorModerationOverridesList() {
+ return Array.from(authorModerationOverrides.values()).map((entry) => ({
+ ...entry,
+ }));
+}
+
+export function getAuthorModerationOverride(pubkey) {
+ const hex = normalizeAuthorOverrideHex(pubkey);
+ if (!hex) {
+ return null;
+ }
+ const entry = authorModerationOverrides.get(hex);
+ return entry ? { ...entry } : null;
+}
+
+export function hasAuthorModerationOverride(pubkey) {
+ const hex = normalizeAuthorOverrideHex(pubkey);
+ if (!hex) {
+ return false;
+ }
+ return authorModerationOverrides.get(hex)?.showAnyway === true;
+}
+
+export function setAuthorModerationOverride(
+ pubkey,
+ override = {},
+ { persist = true } = {},
+) {
+ const hex = normalizeAuthorOverrideHex(pubkey);
+ if (!hex) {
+ return null;
+ }
+
+ const showAnyway = override?.showAnyway === true;
+ if (!showAnyway) {
+ const removed = authorModerationOverrides.delete(hex);
+ if (removed && persist) {
+ persistAuthorModerationOverridesToStorage();
+ }
+ return null;
+ }
+
+ const updatedAt = Number.isFinite(override?.updatedAt)
+ ? Math.floor(override.updatedAt)
+ : Date.now();
+ const existing = authorModerationOverrides.get(hex);
+ const nextEntry = { authorPubkey: hex, showAnyway: true, updatedAt };
+ const changed =
+ !existing ||
+ existing.showAnyway !== true ||
+ existing.updatedAt !== updatedAt;
+
+ authorModerationOverrides.set(hex, nextEntry);
+ if (persist && changed) {
+ persistAuthorModerationOverridesToStorage();
+ }
+ return { ...nextEntry };
+}
+
+export function clearAuthorModerationOverride(pubkey, { persist = true } = {}) {
+ const hex = normalizeAuthorOverrideHex(pubkey);
+ if (!hex) {
+ return false;
+ }
+ const removed = authorModerationOverrides.delete(hex);
+ if (removed && persist) {
+ persistAuthorModerationOverridesToStorage();
+ }
+ return removed;
+}
+
+export function loadAuthorModerationOverridesFromStorage() {
+ if (typeof localStorage === "undefined") {
+ return;
+ }
+ const raw = localStorage.getItem(AUTHOR_MODERATION_OVERRIDE_STORAGE_KEY);
+ if (!raw) {
+ return;
+ }
+ try {
+ const payload = JSON.parse(raw);
+ if (
+ !payload ||
+ typeof payload !== "object" ||
+ payload.version !== AUTHOR_MODERATION_OVERRIDE_STORAGE_VERSION ||
+ !Array.isArray(payload.entries)
+ ) {
+ return;
+ }
+ authorModerationOverrides.clear();
+ for (const entry of payload.entries) {
+ if (!entry || entry.showAnyway !== true) {
+ continue;
+ }
+ const hex = normalizeAuthorOverrideHex(entry.authorPubkey || entry.pubkey);
+ if (!hex) {
+ continue;
+ }
+ const updatedAt = Number.isFinite(entry.updatedAt)
+ ? Math.floor(entry.updatedAt)
+ : Date.now();
+ authorModerationOverrides.set(hex, {
+ authorPubkey: hex,
+ showAnyway: true,
+ updatedAt,
+ });
+ }
+ } catch (error) {
+ authorModerationOverrides.clear();
+ userLogger.warn(
+ "[cache.loadAuthorModerationOverridesFromStorage] Failed to parse payload:",
+ error,
+ );
+ }
+}
+
+export function persistAuthorModerationOverridesToStorage() {
+ if (typeof localStorage === "undefined") {
+ return;
+ }
+ const entries = [];
+ for (const entry of authorModerationOverrides.values()) {
+ if (!entry || entry.showAnyway !== true) {
+ continue;
+ }
+ entries.push({
+ authorPubkey: entry.authorPubkey,
+ showAnyway: true,
+ updatedAt: Number.isFinite(entry.updatedAt)
+ ? Math.floor(entry.updatedAt)
+ : Date.now(),
+ });
+ }
+ try {
+ if (entries.length === 0) {
+ localStorage.removeItem(AUTHOR_MODERATION_OVERRIDE_STORAGE_KEY);
+ return;
+ }
+ localStorage.setItem(
+ AUTHOR_MODERATION_OVERRIDE_STORAGE_KEY,
+ JSON.stringify({
+ version: AUTHOR_MODERATION_OVERRIDE_STORAGE_VERSION,
+ savedAt: Date.now(),
+ entries,
+ }),
+ );
+ } catch (error) {
+ userLogger.warn(
+ "[cache.persistAuthorModerationOverridesToStorage] Failed to persist overrides:",
+ error,
+ );
+ }
+}
+
function buildUrlProbeKey(url, options = {}) {
const trimmed = typeof url === "string" ? url : "";
const mode = options?.confirmPlayable ? "playable" : "basic";
diff --git a/js/ui/applicationBootstrap.js b/js/ui/applicationBootstrap.js
index 1cbec0ab..4d5fd701 100644
--- a/js/ui/applicationBootstrap.js
+++ b/js/ui/applicationBootstrap.js
@@ -58,6 +58,9 @@ import {
getDmPrivacySettings,
setDmPrivacySettings,
clearModerationOverride,
+ getAuthorModerationOverridesList,
+ setAuthorModerationOverride,
+ clearAuthorModerationOverride,
persistSavedProfiles,
getSavedProfiles,
getActiveProfilePubkey,
@@ -93,6 +96,30 @@ import {
} from "./components/staticModalAccessibility.js";
import ModalManager from "./ModalManager.js";
+// After an account-level ("trusted creator") override is set/cleared, refresh
+// the feed so every video by that author re-decorates (warning on/off), and
+// fire the shared moderation-override event so the Safety tab list refreshes.
+function notifyAuthorModerationOverrideChanged(app, pubkey) {
+ try {
+ if (typeof document !== "undefined") {
+ document.dispatchEvent(
+ new CustomEvent("video:moderation-override", {
+ detail: { authorPubkey: pubkey || "", accountLevel: true },
+ }),
+ );
+ }
+ } catch (error) {
+ // best-effort
+ }
+ try {
+ if (typeof app?.onVideosShouldRefresh === "function") {
+ void app.onVideosShouldRefresh({ reason: "author-moderation-override" });
+ }
+ } catch (error) {
+ // best-effort
+ }
+}
+
export default class ApplicationBootstrap {
constructor({
app,
@@ -550,6 +577,22 @@ export default class ApplicationBootstrap {
getModerationOverrides: () => getModerationOverridesList(),
clearModerationOverride: (descriptor) =>
clearModerationOverride(descriptor),
+ // Account-level ("trusted creator") WoT overrides. Setting/clearing
+ // one re-renders the feed so every video by that author updates, and
+ // fires the shared event so the Safety tab list refreshes.
+ getAuthorModerationOverrides: () => getAuthorModerationOverridesList(),
+ setAuthorModerationOverride: (pubkey) => {
+ const entry = setAuthorModerationOverride(pubkey, {
+ showAnyway: true,
+ });
+ notifyAuthorModerationOverrideChanged(app, pubkey);
+ return entry;
+ },
+ clearAuthorModerationOverride: (pubkey) => {
+ const removed = clearAuthorModerationOverride(pubkey);
+ notifyAuthorModerationOverrideChanged(app, pubkey);
+ return removed;
+ },
loadVideos: (forceFetch, context) =>
app.loadVideos(forceFetch, context),
onVideosShouldRefresh: (context) =>
diff --git a/js/ui/profileModal/ProfileModerationController.js b/js/ui/profileModal/ProfileModerationController.js
index 0f9de731..9491cbf2 100644
--- a/js/ui/profileModal/ProfileModerationController.js
+++ b/js/ui/profileModal/ProfileModerationController.js
@@ -47,6 +47,11 @@ export class ProfileModerationController {
this.moderationStatusText = document.getElementById("profileModerationStatus") || null;
this.moderationOverridesList = document.getElementById("profileModerationOverridesList") || null;
this.moderationOverridesEmpty = document.getElementById("profileModerationOverridesEmpty") || null;
+ this.authorOverrideInput = document.getElementById("profileAuthorOverrideInput") || null;
+ this.authorOverrideAddBtn = document.getElementById("profileAuthorOverrideAddBtn") || null;
+ this.authorOverrideStatus = document.getElementById("profileAuthorOverrideStatus") || null;
+ this.authorOverridesList = document.getElementById("profileAuthorOverridesList") || null;
+ this.authorOverridesEmpty = document.getElementById("profileAuthorOverridesEmpty") || null;
this.moderationTrustedContactsCount = document.getElementById("profileModerationTrustedContactsCount") || null;
this.moderationTrustedMuteCount = document.getElementById("profileModerationTrustedMuteCount") || null;
this.moderationTrustedReportCount = document.getElementById("profileModerationTrustedReportCount") || null;
@@ -102,9 +107,24 @@ export class ProfileModerationController {
});
}
+ if (this.authorOverrideAddBtn instanceof HTMLElement) {
+ this.authorOverrideAddBtn.addEventListener("click", () => {
+ void this.handleAddAuthorOverride();
+ });
+ }
+ if (this.authorOverrideInput instanceof HTMLInputElement) {
+ this.authorOverrideInput.addEventListener("keydown", (event) => {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ void this.handleAddAuthorOverride();
+ }
+ });
+ }
+
if (!this.boundModerationOverridesUpdate && typeof document !== "undefined") {
this.boundModerationOverridesUpdate = () => {
this.refreshModerationOverridesUi();
+ this.refreshAuthorOverridesUi();
};
document.addEventListener(
"video:moderation-override",
@@ -569,6 +589,159 @@ export class ProfileModerationController {
}
}
+ setAuthorOverrideStatus(message) {
+ if (this.authorOverrideStatus instanceof HTMLElement) {
+ this.authorOverrideStatus.textContent = message || "";
+ }
+ }
+
+ async handleAddAuthorOverride() {
+ const input =
+ this.authorOverrideInput instanceof HTMLInputElement
+ ? this.authorOverrideInput.value.trim()
+ : "";
+ if (!input) {
+ this.setAuthorOverrideStatus("Enter a creator npub.");
+ return false;
+ }
+ const hex = this.mainController.normalizeHexPubkey(input);
+ if (!hex) {
+ this.setAuthorOverrideStatus("That doesn't look like a valid npub.");
+ return false;
+ }
+ if (
+ typeof this.mainController.services.setAuthorModerationOverride !==
+ "function"
+ ) {
+ return false;
+ }
+ try {
+ this.mainController.services.setAuthorModerationOverride(hex);
+ if (this.authorOverrideInput instanceof HTMLInputElement) {
+ this.authorOverrideInput.value = "";
+ }
+ this.setAuthorOverrideStatus("");
+ this.refreshAuthorOverridesUi();
+ this.mainController.showSuccess("Warnings turned off for this creator.");
+ return true;
+ } catch (error) {
+ this.mainController.showError(
+ "Couldn't add that creator. Please try again.",
+ );
+ return false;
+ }
+ }
+
+ async handleAuthorOverrideReset(entry) {
+ if (
+ !entry ||
+ !entry.authorPubkey ||
+ typeof this.mainController.services.clearAuthorModerationOverride !==
+ "function"
+ ) {
+ return false;
+ }
+ try {
+ this.mainController.services.clearAuthorModerationOverride(
+ entry.authorPubkey,
+ );
+ this.refreshAuthorOverridesUi();
+ this.mainController.showSuccess("Warnings restored for this creator.");
+ return true;
+ } catch (error) {
+ this.mainController.showError("Unable to reset this creator.");
+ return false;
+ }
+ }
+
+ refreshAuthorOverridesUi() {
+ if (
+ !(this.authorOverridesList instanceof HTMLElement) ||
+ !(this.authorOverridesEmpty instanceof HTMLElement)
+ ) {
+ return;
+ }
+
+ const services = this.mainController.services;
+ const raw =
+ typeof services.getAuthorModerationOverrides === "function"
+ ? services.getAuthorModerationOverrides()
+ : [];
+ const entries = (Array.isArray(raw) ? raw : [])
+ .map((entry) => ({
+ authorPubkey:
+ typeof entry?.authorPubkey === "string"
+ ? this.mainController.normalizeHexPubkey(entry.authorPubkey) ||
+ entry.authorPubkey
+ : "",
+ updatedAt: Number.isFinite(entry?.updatedAt)
+ ? Math.floor(entry.updatedAt)
+ : 0,
+ }))
+ .filter((entry) => entry.authorPubkey)
+ .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
+
+ this.authorOverridesList.textContent = "";
+
+ if (!entries.length) {
+ this.authorOverridesEmpty.classList.remove("hidden");
+ this.authorOverridesList.classList.add("hidden");
+ return;
+ }
+
+ this.authorOverridesEmpty.classList.add("hidden");
+ this.authorOverridesList.classList.remove("hidden");
+
+ const entriesNeedingFetch = new Set();
+
+ entries.forEach((entry) => {
+ const item = document.createElement("li");
+ item.className = "card profile-account-card";
+
+ const authorKey = entry.authorPubkey;
+ if (
+ authorKey &&
+ typeof services.getProfileCacheEntry === "function" &&
+ !services.getProfileCacheEntry(authorKey)
+ ) {
+ entriesNeedingFetch.add(authorKey);
+ }
+
+ const summaryData =
+ this.mainController.dmController.resolveProfileSummaryForPubkey(
+ authorKey,
+ );
+ const summary =
+ this.mainController.dmController.createCompactProfileSummary(
+ summaryData,
+ );
+
+ const actions = document.createElement("div");
+ actions.className = "profile-account-card__actions";
+ const resetButton = this.mainController.createRemoveButton({
+ label: "Reset",
+ onRemove: () => this.handleAuthorOverrideReset(entry),
+ });
+
+ if (summary) {
+ item.appendChild(summary);
+ }
+ if (resetButton) {
+ actions.appendChild(resetButton);
+ item.appendChild(actions);
+ }
+
+ this.authorOverridesList.appendChild(item);
+ });
+
+ if (
+ entriesNeedingFetch.size &&
+ typeof services.batchFetchProfiles === "function"
+ ) {
+ services.batchFetchProfiles(entriesNeedingFetch);
+ }
+ }
+
refreshModerationSettingsUi() {
const service = this.getModerationSettingsService();
if (!service) {
@@ -620,6 +793,7 @@ export class ProfileModerationController {
this.updateTrustedHideControlsVisibility();
this.updateModerationTrustStats();
this.refreshModerationOverridesUi();
+ this.refreshAuthorOverridesUi();
this.applyModerationSettingsControlState({ resetStatus: true });
}
diff --git a/js/ui/profileModalContract.js b/js/ui/profileModalContract.js
index 84b340e4..ad941dad 100644
--- a/js/ui/profileModalContract.js
+++ b/js/ui/profileModalContract.js
@@ -510,6 +510,27 @@ const SERVICE_CONTRACT = [
"Clears a specific moderation override when the viewer resets a decision.",
fallback: () => () => false,
},
+ {
+ key: "getAuthorModerationOverrides",
+ type: "function",
+ description:
+ "Returns the account-level (per-creator) web-of-trust overrides for the active viewer.",
+ fallback: () => () => [],
+ },
+ {
+ key: "setAuthorModerationOverride",
+ type: "function",
+ description:
+ "Adds an account-level override that suppresses WoT warnings for all of a creator's videos.",
+ fallback: () => () => null,
+ },
+ {
+ key: "clearAuthorModerationOverride",
+ type: "function",
+ description:
+ "Clears an account-level (per-creator) override so the WoT warnings return.",
+ fallback: () => () => false,
+ },
{
key: "loadVideos",
type: "function",
diff --git a/scripts/check-file-size.mjs b/scripts/check-file-size.mjs
index efe7215f..5060cd3c 100644
--- a/scripts/check-file-size.mjs
+++ b/scripts/check-file-size.mjs
@@ -63,7 +63,7 @@ const GRANDFATHERED = {
"js/services/authService.js": 1335,
"js/ui/zapController.js": 1206,
"js/services/r2Service.js": 1192,
- "js/ui/applicationBootstrap.js": 1217,
+ "js/ui/applicationBootstrap.js": 1451,
"js/ui/components/EditModal.js": 1130,
"js/services/commentThreadService.js": 1100,
"js/nostr/managers/SignerManager.js": 1160,
diff --git a/tests/author-moderation-override.test.mjs b/tests/author-moderation-override.test.mjs
new file mode 100644
index 00000000..c946949f
--- /dev/null
+++ b/tests/author-moderation-override.test.mjs
@@ -0,0 +1,62 @@
+import "./test-helpers/setup-localstorage.mjs";
+import test, { beforeEach } from "node:test";
+import assert from "node:assert/strict";
+
+import {
+ setAuthorModerationOverride,
+ getAuthorModerationOverride,
+ hasAuthorModerationOverride,
+ clearAuthorModerationOverride,
+ getAuthorModerationOverridesList,
+ loadAuthorModerationOverridesFromStorage,
+} from "../js/state/cache.js";
+
+const HEX = "a".repeat(64);
+const HEX2 = "b".repeat(64);
+
+beforeEach(() => {
+ localStorage.clear();
+ clearAuthorModerationOverride(HEX);
+ clearAuthorModerationOverride(HEX2);
+});
+
+test("set + get + has recognizes an account-level override", () => {
+ const entry = setAuthorModerationOverride(HEX, { showAnyway: true });
+ assert.equal(entry.authorPubkey, HEX);
+ assert.equal(entry.showAnyway, true);
+ assert.equal(hasAuthorModerationOverride(HEX), true);
+ assert.equal(getAuthorModerationOverride(HEX)?.showAnyway, true);
+ assert.equal(hasAuthorModerationOverride(HEX2), false);
+});
+
+test("hex input is normalized (uppercase accepted, stored lowercase)", () => {
+ setAuthorModerationOverride(HEX.toUpperCase(), { showAnyway: true });
+ assert.equal(hasAuthorModerationOverride(HEX), true, "uppercase resolves to the same entry");
+});
+
+test("clearing removes it; showAnyway:false is treated as a removal", () => {
+ setAuthorModerationOverride(HEX, { showAnyway: true });
+ assert.equal(clearAuthorModerationOverride(HEX), true);
+ assert.equal(hasAuthorModerationOverride(HEX), false);
+
+ setAuthorModerationOverride(HEX, { showAnyway: true });
+ assert.equal(setAuthorModerationOverride(HEX, { showAnyway: false }), null);
+ assert.equal(hasAuthorModerationOverride(HEX), false);
+});
+
+test("survives a reload via persist → load roundtrip", () => {
+ setAuthorModerationOverride(HEX, { showAnyway: true });
+ setAuthorModerationOverride(HEX2, { showAnyway: true });
+ // A reload repopulates the in-memory map from localStorage.
+ loadAuthorModerationOverridesFromStorage();
+ const list = getAuthorModerationOverridesList()
+ .map((entry) => entry.authorPubkey)
+ .sort();
+ assert.deepEqual(list, [HEX, HEX2].sort());
+});
+
+test("blank/invalid pubkey is ignored", () => {
+ assert.equal(setAuthorModerationOverride("", { showAnyway: true }), null);
+ assert.equal(setAuthorModerationOverride("not-a-key", { showAnyway: true }), null);
+ assert.equal(getAuthorModerationOverridesList().length, 0);
+});