mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Safety: account-level (per-creator) web-of-trust overrides
Add "trusted creator" overrides that suppress the web-of-trust warning/blur for ALL of a creator's videos until reset — the account-level companion to the existing per-video "Show anyway". - cache.js: per-author override store (bitvid:moderationAuthorOverrides:v1), keyed by author hex, with get/set/has/clear/list + persist/load, mirroring the per-video store. - moderationDecorator: overrideActive now also honors an account-level override for the video's author, so the whole class of WoT warnings clears for every video by that creator. - app.js: load the author overrides on startup. - Services + contract: getAuthorModerationOverrides / setAuthorModerationOverride / clearAuthorModerationOverride; set/clear refresh the feed + fire the shared moderation-override event so the Safety tab list updates. - Safety tab: a "Trusted creators" section — add by npub, list each with a Reset, mirroring the Active-overrides section. - tests: storage set/get/has/clear/list, hex normalization, persist→load, invalid-input guard. Next: the "Always show this creator" button on the video warning overlay (second entry point) — a larger multi-component UI change, done separately. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
18e7197bdc
commit
a0131c718e
9 files changed
+538
-5
No files matched your search
@@ -1612,6 +1612,56 @@
|
||||
data-orientation="vertical"
|
||||
></ul>
|
||||
</div>
|
||||
<div class="space-y-3 border-t border-border/60 pt-4">
|
||||
<div class="space-y-1">
|
||||
<h4 class="text-sm font-semibold text-text">
|
||||
Trusted creators
|
||||
</h4>
|
||||
<p class="text-xs text-muted">
|
||||
Turn off web-of-trust warnings for a creator across all
|
||||
their videos. They stay listed here until you reset them.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-end gap-2">
|
||||
<label class="min-w-0 flex-1 space-y-1">
|
||||
<span class="text-xs font-medium text-muted"
|
||||
>Creator npub</span
|
||||
>
|
||||
<input
|
||||
id="profileAuthorOverrideInput"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="npub1…"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
id="profileAuthorOverrideAddBtn"
|
||||
type="button"
|
||||
class="btn focus-ring"
|
||||
>
|
||||
Always show
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
id="profileAuthorOverrideStatus"
|
||||
class="text-xs text-muted"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
></p>
|
||||
<div
|
||||
id="profileAuthorOverridesEmpty"
|
||||
class="card border border-dashed border-border/60 p-4 text-center text-xs text-muted"
|
||||
>
|
||||
No trusted creators yet.
|
||||
</div>
|
||||
<ul
|
||||
id="profileAuthorOverridesList"
|
||||
class="grid-stack hidden"
|
||||
data-orientation="vertical"
|
||||
></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="profileLinkPreviewSettings" class="card space-y-4 p-5">
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
? {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user