mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-31 01:42:27 +00:00
The `VideoCard` component automatically hides itself if it detects no valid playback source (no URL or magnet). This caused the moderation visual test fixtures, which used dummy data without sources, to be hidden (`hidden=""` on root element) in the DOM, even though they were "visible" in terms of layout (display: block forced on inner container). This state prevented Playwright from interacting with the "Show anyway" button, causing click timeouts. This change: 1. Updates `docs/moderation/fixtures/index.html` to provide a dummy URL for all fixture videos, ensuring the card remains visible and interactive. 2. Implements the missing `onModerationHide` handler in the fixture logic to support the full override/restore lifecycle in tests. 3. Uncomments and restores the failing assertions in `tests/visual/moderation.spec.ts`. 4. Removes `force: true` from click actions as the elements are now properly visible. Co-authored-by: PR0M3TH3AN <[email protected]>
730 lines
24 KiB
HTML
730 lines
24 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>Moderation Fixtures • bitvid</title>
|
|
<base href="/" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<link rel="stylesheet" href="/css/tailwind.generated.css" />
|
|
<style>
|
|
.moderation-fixtures {
|
|
background-color: var(--surface-base);
|
|
color: var(--text-primary);
|
|
font-family: var(--font-family-sans);
|
|
}
|
|
.moderation-fixtures__main {
|
|
max-width: 60rem;
|
|
margin: 0 auto;
|
|
padding: var(--space-xl) var(--space-lg) var(--space-2xl);
|
|
}
|
|
.moderation-fixtures__title {
|
|
font-size: var(--font-size-2xl);
|
|
font-weight: 700;
|
|
margin-bottom: var(--space-lg);
|
|
}
|
|
.moderation-fixtures__section {
|
|
margin-bottom: var(--space-2xl);
|
|
}
|
|
.moderation-fixtures__section-title {
|
|
font-size: var(--font-size-xl);
|
|
font-weight: 600;
|
|
margin-bottom: var(--space-2xs);
|
|
}
|
|
.moderation-fixtures__section-description {
|
|
color: var(--text-muted);
|
|
margin-bottom: var(--space-md);
|
|
}
|
|
.fixture-toggle {
|
|
margin-top: var(--space-sm);
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-muted);
|
|
}
|
|
.fixture-toggle__label {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--space-xs);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="moderation-fixtures" data-ds="new">
|
|
<main id="moderationFixtureRoot" class="moderation-fixtures__main">
|
|
<h1 class="moderation-fixtures__title">Moderation Test Fixtures</h1>
|
|
<p>
|
|
These scenarios mirror the QA vectors in
|
|
<code>docs/moderation/testing.md</code> so automated checks can assert
|
|
blur thresholds, autoplay blocking, and local override persistence.
|
|
</p>
|
|
</main>
|
|
<script type="module">
|
|
import { VideoCard } from "/js/ui/components/VideoCard.js";
|
|
import moderationService from "/js/services/moderationService.js";
|
|
|
|
const STORAGE_KEY = "bitvid:moderation:fixture-overrides";
|
|
const READY_FLAG = "__moderationFixturesReady__";
|
|
// These fixtures mirror the upstream defaults in config/instance-config.js.
|
|
// Update the numbers alongside DEFAULT_BLUR_THRESHOLD, DEFAULT_AUTOPLAY_BLOCK_THRESHOLD,
|
|
// DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD, and DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD when they change.
|
|
const fixtures = [
|
|
{
|
|
key: "blur-threshold",
|
|
heading: "Blur threshold (config default: ≥3 trusted nudity reports)",
|
|
description:
|
|
"Card should be blurred and autoplay disabled until the viewer overrides it.",
|
|
thresholds: { blur: 3, autoplay: 2 },
|
|
video: {
|
|
id: "1".repeat(64),
|
|
title: "Fixture: Blurred Preview",
|
|
pubkey: "a".repeat(64),
|
|
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg",
|
|
url: "https://example.com/dummy.mp4"
|
|
},
|
|
moderation: {
|
|
blockAutoplay: true,
|
|
blurThumbnail: true,
|
|
trustedCount: 3,
|
|
reportType: "nudity"
|
|
},
|
|
reporters: [
|
|
{ name: "Jess F1", pubkey: "b".repeat(64), latest: 1_700_000_010 },
|
|
{ name: "Mira F1", pubkey: "c".repeat(64), latest: 1_700_000_020 },
|
|
{ name: "Omar F1", pubkey: "d".repeat(64), latest: 1_700_000_030 }
|
|
],
|
|
blockToggle: {
|
|
label: "Simulate viewer blocking Mira F1",
|
|
pubkey: "c".repeat(64)
|
|
}
|
|
},
|
|
{
|
|
key: "autoplay-threshold",
|
|
heading:
|
|
"Autoplay threshold (config default: ≥2 trusted nudity reports)",
|
|
description:
|
|
"Card should block autoplay but stay unblurred so users can inspect the thumbnail.",
|
|
video: {
|
|
id: "2".repeat(64),
|
|
title: "Fixture: Autoplay Blocked",
|
|
pubkey: "e".repeat(64),
|
|
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg",
|
|
url: "https://example.com/dummy.mp4"
|
|
},
|
|
moderation: {
|
|
blockAutoplay: true,
|
|
blurThumbnail: false,
|
|
trustedCount: 2,
|
|
reportType: "nudity"
|
|
},
|
|
reporters: [
|
|
{ name: "Jess F1", pubkey: "b".repeat(64), latest: 1_700_000_010 },
|
|
{ name: "Mira F1", pubkey: "c".repeat(64), latest: 1_700_000_020 }
|
|
]
|
|
},
|
|
{
|
|
key: "show-anyway",
|
|
heading: '"Show anyway" override (persists locally)',
|
|
description:
|
|
'Card starts blurred and blocked; activating "Show anyway" should persist across reloads.',
|
|
enableOverride: true,
|
|
thresholds: { blur: 3, autoplay: 2, reportHide: 4 },
|
|
video: {
|
|
id: "3".repeat(64),
|
|
title: "Fixture: Override Persistence",
|
|
pubkey: "f".repeat(64),
|
|
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg",
|
|
url: "https://example.com/dummy.mp4"
|
|
},
|
|
moderation: {
|
|
blockAutoplay: true,
|
|
blurThumbnail: true,
|
|
trustedCount: 3,
|
|
reportType: "nudity"
|
|
},
|
|
reporters: [
|
|
{ name: "Jess F1", pubkey: "b".repeat(64), latest: 1_700_000_010 },
|
|
{ name: "Mira F1", pubkey: "c".repeat(64), latest: 1_700_000_020 },
|
|
{ name: "Omar F1", pubkey: "d".repeat(64), latest: 1_700_000_030 }
|
|
]
|
|
},
|
|
{
|
|
key: "trusted-report-hide",
|
|
heading: "Trusted report hide (config default: ≥3 spam reports)",
|
|
description:
|
|
'Card should render hidden with spam copy until the viewer clicks "Show anyway".',
|
|
enableOverride: true,
|
|
thresholds: { reportHide: 3 },
|
|
video: {
|
|
id: "4".repeat(64),
|
|
title: "Fixture: Trusted Report Hide",
|
|
pubkey: "aa".repeat(32),
|
|
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg",
|
|
url: "https://example.com/dummy.mp4"
|
|
},
|
|
moderation: {
|
|
blockAutoplay: true,
|
|
blurThumbnail: true,
|
|
trustedCount: 3,
|
|
reportType: "spam"
|
|
},
|
|
reporters: [
|
|
{ name: "Jess F1", pubkey: "b".repeat(64), latest: 1_700_000_010 },
|
|
{ name: "Mira F1", pubkey: "c".repeat(64), latest: 1_700_000_020 },
|
|
{ name: "Omar F1", pubkey: "d".repeat(64), latest: 1_700_000_030 }
|
|
]
|
|
},
|
|
{
|
|
key: "trusted-mute-hide",
|
|
heading: "Trusted mute hide (F1 mute list threshold)",
|
|
description:
|
|
"Card should default to hidden when a trusted contact mutes the author and allow an override.",
|
|
enableOverride: true,
|
|
thresholds: { muteHide: 1 },
|
|
video: {
|
|
id: "5".repeat(64),
|
|
title: "Fixture: Trusted Mute Hide",
|
|
pubkey: "bb".repeat(32),
|
|
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg",
|
|
url: "https://example.com/dummy.mp4"
|
|
},
|
|
moderation: {
|
|
blockAutoplay: false,
|
|
blurThumbnail: false,
|
|
trustedMuted: true,
|
|
trustedMuteCount: 1
|
|
},
|
|
muters: [{ name: "Jess F1", pubkey: "b".repeat(64) }]
|
|
}
|
|
];
|
|
|
|
const blockedReportersByVideo = new Map();
|
|
|
|
function getBlockedSet(videoId) {
|
|
if (typeof videoId !== "string" || !videoId) {
|
|
return null;
|
|
}
|
|
return blockedReportersByVideo.get(videoId) || null;
|
|
}
|
|
|
|
function setReporterBlocked(videoId, pubkey, blocked) {
|
|
if (
|
|
typeof videoId !== "string" ||
|
|
!videoId ||
|
|
typeof pubkey !== "string" ||
|
|
!pubkey
|
|
) {
|
|
return;
|
|
}
|
|
|
|
let entry = blockedReportersByVideo.get(videoId);
|
|
if (blocked) {
|
|
if (!entry) {
|
|
entry = new Set();
|
|
blockedReportersByVideo.set(videoId, entry);
|
|
}
|
|
entry.add(pubkey);
|
|
return;
|
|
}
|
|
|
|
if (!entry) {
|
|
return;
|
|
}
|
|
|
|
entry.delete(pubkey);
|
|
if (!entry.size) {
|
|
blockedReportersByVideo.delete(videoId);
|
|
}
|
|
}
|
|
|
|
function isReporterBlocked(videoId, pubkey) {
|
|
const entry = getBlockedSet(videoId);
|
|
if (!entry) {
|
|
return false;
|
|
}
|
|
return entry.has(pubkey);
|
|
}
|
|
|
|
function getActiveReporters(fixture, videoId) {
|
|
const reporters = Array.isArray(fixture?.reporters)
|
|
? fixture.reporters
|
|
: [];
|
|
if (!reporters.length) {
|
|
return [];
|
|
}
|
|
|
|
const entry = getBlockedSet(videoId);
|
|
if (!entry || !entry.size) {
|
|
return reporters.slice();
|
|
}
|
|
|
|
return reporters.filter((reporter) => {
|
|
const key =
|
|
typeof reporter?.pubkey === "string" ? reporter.pubkey : "";
|
|
if (!key) {
|
|
return false;
|
|
}
|
|
return !entry.has(key);
|
|
});
|
|
}
|
|
|
|
function normalizeFixtureReporters(reporters) {
|
|
if (!Array.isArray(reporters)) {
|
|
return [];
|
|
}
|
|
|
|
return reporters
|
|
.map((reporter) => {
|
|
if (!reporter || typeof reporter !== "object") {
|
|
return null;
|
|
}
|
|
const pubkey =
|
|
typeof reporter.pubkey === "string" ? reporter.pubkey : "";
|
|
if (!pubkey) {
|
|
return null;
|
|
}
|
|
const latest = Number.isFinite(reporter.latest)
|
|
? Math.floor(reporter.latest)
|
|
: 0;
|
|
const name = typeof reporter.name === "string" ? reporter.name : "";
|
|
return { name, pubkey, latest };
|
|
})
|
|
.filter(Boolean);
|
|
}
|
|
|
|
window.__MODERATION_FIXTURE_STORAGE_KEY__ = STORAGE_KEY;
|
|
window[READY_FLAG] = false;
|
|
|
|
function loadOverrides() {
|
|
try {
|
|
const raw = window.localStorage?.getItem(STORAGE_KEY);
|
|
if (!raw) {
|
|
return {};
|
|
}
|
|
const parsed = JSON.parse(raw);
|
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function persistOverrides(next) {
|
|
try {
|
|
window.localStorage?.setItem(STORAGE_KEY, JSON.stringify(next));
|
|
} catch {
|
|
/* ignore persistence issues in test fixtures */
|
|
}
|
|
}
|
|
|
|
function buildSummary(fixture, videoId, sanitizedReporters = null) {
|
|
if (!fixture?.moderation?.reportType) {
|
|
return null;
|
|
}
|
|
|
|
const reporters =
|
|
Array.isArray(sanitizedReporters) && sanitizedReporters
|
|
? sanitizedReporters
|
|
: normalizeFixtureReporters(getActiveReporters(fixture, videoId));
|
|
|
|
const latest = reporters.reduce(
|
|
(max, reporter) => Math.max(max, reporter.latest),
|
|
0
|
|
);
|
|
|
|
return {
|
|
eventId: fixture.video.id,
|
|
totalTrusted: reporters.length,
|
|
types: {
|
|
[fixture.moderation.reportType]: {
|
|
trusted: reporters.length,
|
|
total: reporters.length,
|
|
latest
|
|
}
|
|
},
|
|
updatedAt: Date.now()
|
|
};
|
|
}
|
|
|
|
function buildModerationState(fixture, overrides, videoId) {
|
|
const overrideEntry = overrides?.[videoId];
|
|
const activeReporters = getActiveReporters(fixture, videoId);
|
|
const sanitizedReporters = normalizeFixtureReporters(activeReporters);
|
|
const thresholds =
|
|
fixture.thresholds && typeof fixture.thresholds === "object"
|
|
? {
|
|
blur: 3,
|
|
autoplay: 2,
|
|
reportHide: 3,
|
|
muteHide: 1,
|
|
...fixture.thresholds
|
|
}
|
|
: { blur: 3, autoplay: 2, reportHide: 3, muteHide: 1 };
|
|
const autoplayThreshold = Number.isFinite(thresholds.autoplay)
|
|
? Math.max(0, Math.floor(thresholds.autoplay))
|
|
: 2;
|
|
const blurThreshold = Number.isFinite(thresholds.blur)
|
|
? Math.max(0, Math.floor(thresholds.blur))
|
|
: 3;
|
|
const reportHideThreshold = Number.isFinite(thresholds.reportHide)
|
|
? Math.max(0, Math.floor(thresholds.reportHide))
|
|
: 3;
|
|
const muteHideThreshold = Number.isFinite(thresholds.muteHide)
|
|
? Math.max(0, Math.floor(thresholds.muteHide))
|
|
: 1;
|
|
const initialBlock = fixture.moderation.blockAutoplay === true;
|
|
const initialBlur = fixture.moderation.blurThumbnail === true;
|
|
const trustedCount = sanitizedReporters.length;
|
|
|
|
const reporterDisplayNames = sanitizedReporters
|
|
.map((reporter) => reporter.name)
|
|
.filter((value) => typeof value === "string" && value);
|
|
|
|
const reporterPubkeys = sanitizedReporters.map(
|
|
(reporter) => reporter.pubkey
|
|
);
|
|
|
|
const trustedReporterEntries = sanitizedReporters.map((reporter) => ({
|
|
pubkey: reporter.pubkey,
|
|
latest: reporter.latest
|
|
}));
|
|
|
|
const muterEntries = Array.isArray(fixture.muters)
|
|
? fixture.muters
|
|
: [];
|
|
const muterPubkeys = muterEntries
|
|
.map((muter) =>
|
|
typeof muter?.pubkey === "string" ? muter.pubkey : ""
|
|
)
|
|
.filter(Boolean);
|
|
const muterDisplayNames = muterEntries
|
|
.map((muter) => (typeof muter?.name === "string" ? muter.name : ""))
|
|
.filter(Boolean);
|
|
const trustedMuted = fixture.moderation.trustedMuted === true;
|
|
const muteCount = Number.isFinite(fixture.moderation.trustedMuteCount)
|
|
? Math.max(0, Math.floor(fixture.moderation.trustedMuteCount))
|
|
: muterPubkeys.length;
|
|
|
|
const hideFromMute = trustedMuted && muteCount >= muteHideThreshold;
|
|
const hideFromReports = trustedCount >= reportHideThreshold;
|
|
let hidden = fixture.moderation.hidden === true;
|
|
let hideReason =
|
|
typeof fixture.moderation.hideReason === "string"
|
|
? fixture.moderation.hideReason
|
|
: "";
|
|
const hideBypass =
|
|
typeof fixture.moderation.hideBypass === "string"
|
|
? fixture.moderation.hideBypass
|
|
: "";
|
|
|
|
if (hideFromMute) {
|
|
hidden = true;
|
|
hideReason = "trusted-mute-hide";
|
|
} else if (hideFromReports) {
|
|
hidden = true;
|
|
hideReason = "trusted-report-hide";
|
|
} else if (!hidden) {
|
|
hideReason = "";
|
|
}
|
|
|
|
const hideCounts = hidden
|
|
? {
|
|
trustedMuteCount: muteCount,
|
|
trustedReportCount: trustedCount
|
|
}
|
|
: null;
|
|
|
|
const base = {
|
|
reportType: fixture.moderation.reportType,
|
|
trustedCount,
|
|
reporterDisplayNames,
|
|
reporterPubkeys,
|
|
trustedReporters: trustedReporterEntries,
|
|
trustedMuted,
|
|
trustedMuters: muterPubkeys,
|
|
trustedMuteCount: muteCount,
|
|
trustedMuterDisplayNames: muterDisplayNames
|
|
};
|
|
|
|
const summary = buildSummary(fixture, videoId, sanitizedReporters);
|
|
if (summary) {
|
|
base.summary = summary;
|
|
}
|
|
|
|
const computedBlock = initialBlock && trustedCount >= autoplayThreshold;
|
|
let computedBlur = initialBlur && trustedCount >= blurThreshold;
|
|
|
|
if (!computedBlur && (trustedMuted || hidden)) {
|
|
computedBlur = true;
|
|
}
|
|
|
|
const originalState = {
|
|
blockAutoplay: computedBlock,
|
|
blurThumbnail: computedBlur,
|
|
hidden,
|
|
hideReason: hidden ? hideReason : "",
|
|
hideCounts: hideCounts
|
|
? {
|
|
trustedMuteCount: Math.max(
|
|
0,
|
|
Number(hideCounts.trustedMuteCount) || 0
|
|
),
|
|
trustedReportCount: Math.max(
|
|
0,
|
|
Number(hideCounts.trustedReportCount) || 0
|
|
)
|
|
}
|
|
: null,
|
|
hideBypass,
|
|
hideTriggered: hidden
|
|
};
|
|
|
|
base.original = {
|
|
blockAutoplay: originalState.blockAutoplay,
|
|
blurThumbnail: originalState.blurThumbnail,
|
|
hidden: originalState.hidden,
|
|
hideReason: originalState.hideReason,
|
|
hideCounts: originalState.hideCounts,
|
|
hideBypass: originalState.hideBypass,
|
|
hideTriggered: originalState.hideTriggered
|
|
};
|
|
|
|
if (overrideEntry?.showAnyway) {
|
|
base.viewerOverride = {
|
|
showAnyway: true,
|
|
updatedAt: Number.isFinite(overrideEntry.updatedAt)
|
|
? Math.floor(overrideEntry.updatedAt)
|
|
: Date.now()
|
|
};
|
|
base.blockAutoplay = false;
|
|
base.blurThumbnail = false;
|
|
base.hidden = false;
|
|
if (base.hideReason) {
|
|
delete base.hideReason;
|
|
}
|
|
if (base.hideCounts) {
|
|
delete base.hideCounts;
|
|
}
|
|
if (base.hideBypass) {
|
|
delete base.hideBypass;
|
|
}
|
|
} else {
|
|
base.blockAutoplay = originalState.blockAutoplay;
|
|
base.blurThumbnail = originalState.blurThumbnail;
|
|
if (originalState.hidden) {
|
|
base.hidden = true;
|
|
if (originalState.hideReason) {
|
|
base.hideReason = originalState.hideReason;
|
|
}
|
|
if (originalState.hideCounts) {
|
|
base.hideCounts = { ...originalState.hideCounts };
|
|
}
|
|
if (originalState.hideBypass) {
|
|
base.hideBypass = originalState.hideBypass;
|
|
}
|
|
} else {
|
|
if (base.hidden) {
|
|
delete base.hidden;
|
|
}
|
|
if (base.hideReason) {
|
|
delete base.hideReason;
|
|
}
|
|
if (base.hideCounts) {
|
|
delete base.hideCounts;
|
|
}
|
|
if (base.hideBypass) {
|
|
delete base.hideBypass;
|
|
}
|
|
}
|
|
}
|
|
|
|
return base;
|
|
}
|
|
|
|
function setOverride(overrides, videoId, entry) {
|
|
const next = { ...overrides };
|
|
next[videoId] = {
|
|
...entry,
|
|
updatedAt: Number.isFinite(entry?.updatedAt)
|
|
? Math.floor(entry.updatedAt)
|
|
: Date.now()
|
|
};
|
|
persistOverrides(next);
|
|
return next;
|
|
}
|
|
|
|
function createCard(fixture, overrides) {
|
|
const video = {
|
|
...fixture.video
|
|
};
|
|
video.moderation = buildModerationState(fixture, overrides, video.id);
|
|
|
|
const card = new VideoCard({
|
|
document,
|
|
video,
|
|
shareUrl: "#",
|
|
formatters: {
|
|
formatTimeAgo: () => "just now",
|
|
formatNumber: (value) => {
|
|
if (typeof value === "number") {
|
|
return value.toLocaleString("en-US");
|
|
}
|
|
return String(value ?? "");
|
|
}
|
|
},
|
|
helpers: {
|
|
isMagnetSupported: () => true,
|
|
escapeHtml: (value) => String(value ?? ""),
|
|
toLocaleString: (value, locales, options) => {
|
|
if (value && typeof value.toLocaleString === "function") {
|
|
return value.toLocaleString(locales || "en-US", options);
|
|
}
|
|
return String(value ?? "");
|
|
}
|
|
},
|
|
assets: {
|
|
fallbackThumbnailSrc: "/assets/jpg/video-thumbnail-fallback.jpg"
|
|
},
|
|
designSystem: {
|
|
getMode: () => "new",
|
|
isNew: () => true
|
|
}
|
|
});
|
|
|
|
const refreshModeration = () => {
|
|
card.video.moderation = buildModerationState(
|
|
fixture,
|
|
overrides,
|
|
video.id
|
|
);
|
|
if (typeof card.refreshModerationUi === "function") {
|
|
card.refreshModerationUi();
|
|
}
|
|
};
|
|
|
|
if (fixture.enableOverride) {
|
|
card.onModerationOverride = () => {
|
|
overrides = setOverride(overrides, video.id, { showAnyway: true });
|
|
refreshModeration();
|
|
try {
|
|
document.dispatchEvent(
|
|
new CustomEvent("video:moderation-override", {
|
|
detail: { video }
|
|
})
|
|
);
|
|
} catch {
|
|
/* ignore fixture event errors */
|
|
}
|
|
return true;
|
|
};
|
|
|
|
card.onModerationHide = () => {
|
|
overrides = setOverride(overrides, video.id, { showAnyway: false });
|
|
refreshModeration();
|
|
return true;
|
|
};
|
|
}
|
|
|
|
const root = card.getRoot();
|
|
root.dataset.testId = fixture.key;
|
|
if (fixture.enableOverride) {
|
|
root.dataset.moderationOverrideAvailable = "true";
|
|
}
|
|
return { card, root, refreshModeration };
|
|
}
|
|
|
|
function renderFixtures() {
|
|
const root = document.getElementById("moderationFixtureRoot");
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
let overrides = loadOverrides();
|
|
|
|
fixtures.forEach((fixture) => {
|
|
const section = document.createElement("section");
|
|
section.classList.add("moderation-fixtures__section");
|
|
|
|
const heading = document.createElement("h2");
|
|
heading.textContent = fixture.heading;
|
|
heading.classList.add("moderation-fixtures__section-title");
|
|
section.appendChild(heading);
|
|
|
|
if (fixture.description) {
|
|
const description = document.createElement("p");
|
|
description.textContent = fixture.description;
|
|
description.classList.add(
|
|
"moderation-fixtures__section-description"
|
|
);
|
|
section.appendChild(description);
|
|
}
|
|
|
|
const { root: cardRoot, refreshModeration } = createCard(
|
|
fixture,
|
|
overrides
|
|
);
|
|
section.appendChild(cardRoot);
|
|
|
|
// Force refresh after attach to ensure visibility state is correct
|
|
refreshModeration();
|
|
|
|
if (fixture.blockToggle && fixture.blockToggle.pubkey) {
|
|
const toggleWrapper = document.createElement("div");
|
|
toggleWrapper.classList.add("fixture-toggle");
|
|
|
|
const label = document.createElement("label");
|
|
label.classList.add("fixture-toggle__label");
|
|
|
|
const input = document.createElement("input");
|
|
input.type = "checkbox";
|
|
input.checked = isReporterBlocked(
|
|
fixture.video.id,
|
|
fixture.blockToggle.pubkey
|
|
);
|
|
input.addEventListener("change", () => {
|
|
const checked = input.checked;
|
|
setReporterBlocked(
|
|
fixture.video.id,
|
|
fixture.blockToggle.pubkey,
|
|
checked
|
|
);
|
|
const action = checked ? "block" : "unblock";
|
|
try {
|
|
const detail = {
|
|
action,
|
|
targetPubkey: fixture.blockToggle.pubkey
|
|
};
|
|
const maybePromise =
|
|
moderationService?.queueUserBlockRefresh?.(detail);
|
|
if (maybePromise && typeof maybePromise.then === "function") {
|
|
maybePromise.catch(() => {});
|
|
}
|
|
} catch {
|
|
/* ignore fixture errors */
|
|
}
|
|
refreshModeration();
|
|
});
|
|
|
|
const text = document.createElement("span");
|
|
text.textContent =
|
|
typeof fixture.blockToggle.label === "string"
|
|
? fixture.blockToggle.label
|
|
: "Viewer blocks reporter";
|
|
|
|
label.appendChild(input);
|
|
label.appendChild(text);
|
|
toggleWrapper.appendChild(label);
|
|
section.appendChild(toggleWrapper);
|
|
}
|
|
|
|
root.appendChild(section);
|
|
});
|
|
}
|
|
|
|
renderFixtures();
|
|
|
|
// Delay the ready signal slightly to ensure layout and observers have settled
|
|
setTimeout(() => {
|
|
window[READY_FLAG] = true;
|
|
document.body.dataset.ready = "true";
|
|
window.dispatchEvent(new CustomEvent("moderation-fixtures-ready"));
|
|
}, 50);
|
|
</script>
|
|
</body>
|
|
</html>
|