Fix viewer mute blur regression and update modal overrides

This commit is contained in:
thePR0M3TH3AN committed 2025-10-26 16:06:40 -04:00
1 parent 3936b1ed9f
commit 9dfe7ef20e
5 files changed
+193 -11

No files matched your search

+34 -4
View File
@@ -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,
+24 -3
View File
@@ -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,
+13 -4
View File
@@ -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) {
+70
View File
@@ -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);
+52
View File
@@ -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);