From 9dfe7ef20efb64db409485dbdf66ad66bdd41483 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 26 Oct 2025 16:06:40 -0400 Subject: [PATCH] Fix viewer mute blur regression and update modal overrides --- js/app.js | 38 +++++++++++++-- js/feedEngine/stages.js | 27 +++++++++-- js/ui/components/VideoModal.js | 17 +++++-- tests/moderation-stage.test.mjs | 70 ++++++++++++++++++++++++++++ tests/moderation/video-card.test.mjs | 52 +++++++++++++++++++++ 5 files changed, 193 insertions(+), 11 deletions(-) diff --git a/js/app.js b/js/app.js index 5730d5b2..bb3b6368 100644 --- a/js/app.js +++ b/js/app.js @@ -7704,12 +7704,34 @@ class Application { ? Math.max(0, Math.floor(existingModeration.trustedCount)) : this.deriveModerationTrustedCount(summary, reportType); + const viewerMuted = existingModeration.viewerMuted === true; + const existingBlockAutoplay = existingModeration.blockAutoplay === true; + const existingBlurThumbnail = existingModeration.blurThumbnail === true; + const existingBlurReason = + typeof existingModeration.blurReason === "string" + ? existingModeration.blurReason.trim() + : ""; + const thresholds = this.getActiveModerationThresholds(); - const computedBlockAutoplay = + const computedBlockAutoplayBase = trustedCount >= thresholds.autoplayBlockThreshold || trustedMuted; + const computedBlockAutoplay = + computedBlockAutoplayBase || viewerMuted || existingBlockAutoplay; + const blurFromReports = trustedCount >= thresholds.blurThreshold; - let computedBlurThumbnail = blurFromReports; - let computedBlurReason = computedBlurThumbnail ? "trusted-report" : ""; + let computedBlurThumbnail = + blurFromReports || trustedMuted || viewerMuted || existingBlurThumbnail; + let computedBlurReason = ""; + + if (blurFromReports) { + computedBlurReason = "trusted-report"; + } else if (trustedMuted) { + computedBlurReason = "trusted-mute"; + } else if (viewerMuted) { + computedBlurReason = "viewer-mute"; + } else if (existingBlurThumbnail && existingBlurReason) { + computedBlurReason = existingBlurReason; + } const muteHideThreshold = Number.isFinite(thresholds.trustedMuteHideThreshold) ? Math.max(0, Math.floor(thresholds.trustedMuteHideThreshold)) @@ -7745,16 +7767,20 @@ class Application { hideTriggered = true; } - if (!computedBlurThumbnail && (trustedMuted || hideTriggered)) { + if (!computedBlurThumbnail && (viewerMuted || trustedMuted || hideTriggered)) { computedBlurThumbnail = true; if (hideTriggered) { computedBlurReason = hideReason || "trusted-hide"; + } else if (viewerMuted) { + computedBlurReason = "viewer-mute"; } else if (trustedMuted) { computedBlurReason = "trusted-mute"; } } else if (computedBlurThumbnail) { if (hideTriggered) { computedBlurReason = hideReason || "trusted-hide"; + } else if (viewerMuted && !blurFromReports && !trustedMuted) { + computedBlurReason = "viewer-mute"; } else if (trustedMuted && !blurFromReports) { computedBlurReason = "trusted-mute"; } else if (!computedBlurReason && blurFromReports) { @@ -7762,6 +7788,10 @@ class Application { } } + if (computedBlurThumbnail && !computedBlurReason && existingBlurReason) { + computedBlurReason = existingBlurReason; + } + const hideCounts = hideTriggered ? { trustedMuteCount, diff --git a/js/feedEngine/stages.js b/js/feedEngine/stages.js index 671b1fef..ef0fe569 100644 --- a/js/feedEngine/stages.js +++ b/js/feedEngine/stages.js @@ -768,6 +768,7 @@ export function createModerationStage({ let trustedMuted = false; let trustedMuters = []; let trustedMuteCount = 0; + let viewerMuted = false; if (videoId) { try { if (typeof resolvedService.getTrustedReportSummary === "function") { @@ -827,10 +828,14 @@ export function createModerationStage({ }); } } + if (typeof resolvedService.isAuthorMutedByViewer === "function") { + viewerMuted = resolvedService.isAuthorMutedByViewer(authorHex) === true; + } } catch (error) { - context?.log?.(`[${stageName}] Failed to resolve trusted mute info`, error); + context?.log?.(`[${stageName}] Failed to resolve trusted/viewer mute info`, error); trustedMuted = false; trustedMuters = []; + viewerMuted = false; } } @@ -850,7 +855,7 @@ export function createModerationStage({ } const blockAutoplay = - trustedCount >= normalizedAutoplayThreshold || trustedMuted; + trustedCount >= normalizedAutoplayThreshold || trustedMuted || viewerMuted; const blurFromReports = trustedCount >= normalizedBlurThreshold; let blurThumbnail = blurFromReports; let blurReason = blurThumbnail ? "trusted-report" : ""; @@ -878,6 +883,7 @@ export function createModerationStage({ metadataModeration.trustedMuted = trustedMuted; metadataModeration.trustedMuters = trustedMuters.slice(); metadataModeration.trustedMuteCount = trustedMuteCount; + metadataModeration.viewerMuted = viewerMuted; if (!video.moderation || typeof video.moderation !== "object") { video.moderation = {}; @@ -900,6 +906,7 @@ export function createModerationStage({ delete video.moderation.trustedMuteCount; } } + video.moderation.viewerMuted = viewerMuted; if (Array.isArray(trustedReporters) && trustedReporters.length) { video.moderation.trustedReporters = trustedReporters.slice(); } else if (video.moderation.trustedReporters) { @@ -980,16 +987,20 @@ export function createModerationStage({ } } - if (!blurThumbnail && (trustedMuted || hideTriggered)) { + if (!blurThumbnail && (viewerMuted || trustedMuted || hideTriggered)) { blurThumbnail = true; if (hideTriggered) { blurReason = hideReason || "trusted-hide"; + } else if (viewerMuted) { + blurReason = "viewer-mute"; } else if (trustedMuted) { blurReason = "trusted-mute"; } } else if (blurThumbnail) { if (hideTriggered) { blurReason = hideReason || "trusted-hide"; + } else if (viewerMuted && !blurFromReports) { + blurReason = "viewer-mute"; } else if (trustedMuted && !blurFromReports) { blurReason = "trusted-mute"; } else if (!blurReason && blurFromReports) { @@ -1048,6 +1059,16 @@ export function createModerationStage({ }); } + if (viewerMuted) { + context?.addWhy?.({ + stage: stageName, + type: "moderation", + reason: "viewer-mute", + videoId: videoId || null, + pubkey: authorHex || authorPubkey || null, + }); + } + if (hideTriggered) { const hideDetail = { stage: stageName, diff --git a/js/ui/components/VideoModal.js b/js/ui/components/VideoModal.js index 049835a7..c13d4295 100644 --- a/js/ui/components/VideoModal.js +++ b/js/ui/components/VideoModal.js @@ -3989,11 +3989,11 @@ export class VideoModal { const badge = this.moderationBadge; const textEl = this.moderationBadgeText; - const shouldShow = Boolean(context?.shouldShow); + const showModeration = Boolean(context?.shouldShow); const shouldBlur = Boolean(context?.activeBlur && !context?.overrideActive); const hiddenActive = Boolean(context?.activeHidden && !context?.overrideActive); - if (!shouldShow) { + if (!showModeration) { if (stage && stage.dataset.visualState === "blurred") { delete stage.dataset.visualState; } @@ -4031,7 +4031,11 @@ export class VideoModal { } if (overlay) { - overlay.removeAttribute("hidden"); + if (showModeration && !context?.overrideActive) { + overlay.removeAttribute("hidden"); + } else { + overlay.setAttribute("hidden", ""); + } overlay.dataset.overlayState = context?.overrideActive ? "override" : "active"; } @@ -4129,7 +4133,12 @@ export class VideoModal { const actions = this.ensureModerationActionsContainer(); let actionsAttached = false; - if (context?.allowOverride) { + const allowOverride = + context?.overrideActive === true || + context?.allowOverride === true || + (shouldBlur && !context?.overrideActive && context?.blurReason !== "viewer-mute"); + + if (allowOverride) { const mode = context.overrideActive ? "hide" : "override"; const primaryButton = this.ensureModerationPrimaryButton(mode); if (primaryButton) { diff --git a/tests/moderation-stage.test.mjs b/tests/moderation-stage.test.mjs index 05026ed5..538d42ef 100644 --- a/tests/moderation-stage.test.mjs +++ b/tests/moderation-stage.test.mjs @@ -496,6 +496,76 @@ test("moderation stage propagates whitelist, muters, and threshold updates", asy assert.equal(updatedMuted.video.moderation.blurReason, undefined); }); +test("moderation stage blurs viewer-muted authors", async () => { + const mutedHex = "f".repeat(64); + + const service = { + async refreshViewerFromClient() {}, + async setActiveEventIds() {}, + getAdminListSnapshot() { + return { whitelist: new Set(), whitelistHex: new Set(), blacklist: new Set(), blacklistHex: new Set() }; + }, + getAccessControlStatus(identifier) { + return { hex: identifier, whitelisted: false, blacklisted: false }; + }, + getTrustedReportSummary() { + return null; + }, + trustedReportCount() { + return 0; + }, + getTrustedReporters() { + return []; + }, + isAuthorMutedByTrusted() { + return false; + }, + isAuthorMutedByViewer(pubkey) { + return pubkey === mutedHex; + }, + }; + + const stage = createModerationStage({ + service, + autoplayThreshold: Number.POSITIVE_INFINITY, + blurThreshold: Number.POSITIVE_INFINITY, + trustedMuteHideThreshold: Number.POSITIVE_INFINITY, + trustedReportHideThreshold: Number.POSITIVE_INFINITY, + }); + + const items = [{ video: { id: "viewer-muted", pubkey: mutedHex }, metadata: {} }]; + + const reasons = []; + const context = { + addWhy(detail) { + reasons.push(detail); + return detail; + }, + log() {}, + }; + + const result = await stage(items, context); + + assert.equal(result.length, 1); + const entry = result[0]; + + assert.equal(entry.video.moderation.viewerMuted, true); + assert.equal(entry.metadata.moderation.viewerMuted, true); + assert.equal(entry.video.moderation.blockAutoplay, true); + assert.equal(entry.video.moderation.blurThumbnail, true); + assert.equal(entry.video.moderation.blurReason, "viewer-mute"); + assert.equal(entry.metadata.moderation.blurReason, "viewer-mute"); + assert.equal(entry.video.moderation.hidden, false); + assert.equal(entry.metadata.moderation.hidden, false); + assert.equal(entry.video.moderation.hideReason, undefined); + assert.equal(entry.metadata.moderation.hideReason, undefined); + assert.equal(entry.metadata.moderation.hideCounts, undefined); + + const reasonSet = new Set(reasons.map((detail) => detail.reason)); + assert(reasonSet.has("blur")); + assert(reasonSet.has("viewer-mute")); +}); + test("moderation stage clears cached reporters and muters after service signals", async () => { const reporterHex = "d".repeat(64); const muterHex = "e".repeat(64); diff --git a/tests/moderation/video-card.test.mjs b/tests/moderation/video-card.test.mjs index 0a5e11bb..ad05f5fd 100644 --- a/tests/moderation/video-card.test.mjs +++ b/tests/moderation/video-card.test.mjs @@ -271,6 +271,58 @@ test( }, ); +test("VideoCard blurs viewer-muted creators", async (t) => { + const { document } = setupDom(t); + withMockedNostrTools(t); + + const app = await createModerationAppHarness(); + const videoId = "d".repeat(64); + + const video = { + id: videoId, + title: "Muted Creator Clip", + pubkey: "e".repeat(64), + moderation: { + viewerMuted: true, + blockAutoplay: true, + blurThumbnail: true, + blurReason: "viewer-mute", + trustedCount: 0, + trustedMuted: false, + trustedReporters: [], + }, + }; + + app.videosMap.set(video.id, video); + app.currentVideo = video; + app.decorateVideoModeration(video); + + const card = new VideoCard({ + document, + video, + formatters: { + formatTimeAgo: () => "moments ago", + }, + helpers: { + isMagnetSupported: () => false, + }, + }); + + document.body.appendChild(card.getRoot()); + + assert.equal(card.getRoot().dataset.autoplayPolicy, "blocked"); + assert.equal(card.thumbnailEl.dataset.thumbnailState, "blurred"); + + const context = normalizeVideoModerationContext(card.video?.moderation); + assert.equal(context.blurReason, "viewer-mute"); + assert.equal(context.activeBlur, true); + assert.equal(context.activeBlockAutoplay, true); + assert.equal(context.trustedCount, 0); + assert.equal(card.getRoot().dataset.moderationReportCount, undefined); + assert.ok(card.moderationBadgeEl); + assert.equal(card.moderationBadgeEl.dataset.moderationState, "blocked"); +}); + test("VideoCard blurs thumbnails when trusted mute triggers without reports", async (t) => { const { document } = setupDom(t); withMockedNostrTools(t);