Merge pull request #1199 from PR0M3TH3AN/video-card-moderation-badge-overlay-5905417972080224775

Overlay moderation badge on blurred thumbnails
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-12 23:32:16 -05:00
commit 8755ad64cd
3 files changed
+153 -14

No files matched your search

+90
View File
@@ -1,6 +1,8 @@
import { normalizeDesignSystemContext } from "../../designSystem.js";
import { formatShortNpub } from "../../utils/formatters.js";
import { sanitizeProfileMediaUrl } from "../../utils/profileMedia.js";
import { normalizeVideoModerationContext } from "../moderationUiHelpers.js";
import { buildModerationBadgeText } from "../moderationCopy.js";
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
const DEFAULT_PROFILE_AVATAR = "assets/svg/default-profile.svg";
@@ -69,6 +71,10 @@ export class SimilarContentCard {
this.viewCountEl = null;
this.discussionCountEl = null;
this.moderationBadgeEl = null;
this.moderationBadgeLabelEl = null;
this.moderationBadgeTextEl = null;
this.build();
}
@@ -342,6 +348,7 @@ export class SimilarContentCard {
root.appendChild(content);
}
this.refreshModerationUi();
this.applyPointerDatasets();
this.bindEvents();
}
@@ -361,6 +368,89 @@ export class SimilarContentCard {
return anchor;
}
getModerationContext() {
return normalizeVideoModerationContext(this.video?.moderation);
}
buildModerationBadge(context = this.getModerationContext()) {
if (!context.shouldShow || !context.trustedMuted) {
return null;
}
const badge = this.document.createElement("div");
badge.className = "moderation-badge opacity-95";
badge.dataset.variant = "warning";
badge.dataset.moderationBadge = "true";
badge.dataset.moderationState = "trusted-mute";
// Position it centrally over the thumbnail like in VideoCard
badge.style.pointerEvents = "auto";
const label = this.document.createElement("span");
label.className = "moderation-badge__label inline-flex items-center gap-xs";
// Simplified icon for small card
const iconWrapper = this.document.createElement("span");
iconWrapper.className = "moderation-badge__icon";
iconWrapper.setAttribute("aria-hidden", "true");
const svg = this.document.createElementNS(SVG_NAMESPACE, "svg");
svg.setAttribute("viewBox", "0 0 20 20");
svg.setAttribute("focusable", "false");
svg.classList.add("moderation-badge__icon-mark");
const path = this.document.createElementNS(SVG_NAMESPACE, "path");
path.setAttribute("fill", "currentColor");
path.setAttribute("d", "M10 18a8 8 0 100-16 8 8 0 000 16zm-.75-11.75a.75.75 0 011.5 0v4.5a.75.75 0 01-1.5 0v-4.5zm.75 8.5a1 1 0 100-2 1 1 0 000 2z");
path.setAttribute("fill-rule", "evenodd");
path.setAttribute("clip-rule", "evenodd");
svg.appendChild(path);
iconWrapper.appendChild(svg);
label.appendChild(iconWrapper);
const text = this.document.createElement("span");
text.className = "moderation-badge__text";
// Shorten text for small cards if needed, but keeping standard for now
text.textContent = "Blocked by trusted contact";
label.appendChild(text);
badge.appendChild(label);
this.moderationBadgeEl = badge;
return badge;
}
refreshModerationUi() {
const context = this.getModerationContext();
if (this.moderationBadgeEl) {
this.moderationBadgeEl.remove();
this.moderationBadgeEl = null;
}
// Check specifically for muted/blurred state where we want the overlay
if (context.shouldShow && context.trustedMuted && !context.overrideActive) {
// Ensure media link exists
if (!this.mediaLinkEl) return;
const badge = this.buildModerationBadge(context);
if (badge) {
// Create a wrapper for centering if needed, or use classes on badge if possible.
// Similar to VideoCard, let's create a container for absolute positioning
const container = this.document.createElement("div");
container.className = "absolute inset-0 flex items-center justify-center p-2 z-10 pointer-events-none";
container.appendChild(badge);
this.mediaLinkEl.appendChild(container);
// We track the container removal via moderationBadgeEl reference if we wanted strict cleanup,
// but for now just rebuilding is fine if we store the container.
// Let's store badge el as the container actually to make removal easier
this.moderationBadgeEl = container;
}
}
}
buildThumbnail() {
const img = this.document.createElement("img");
img.decoding = "async";
+49 -13
View File
@@ -493,14 +493,28 @@ export class VideoCard {
}
const moderationBadgeSlot = this.createElement("div", {
classNames: ["video-card__moderation-slot", "px-md", "pt-md"],
classNames: [
"video-card__moderation-overlay",
"absolute",
"inset-0",
"flex",
"items-center",
"justify-center",
"p-4",
"z-10",
"pointer-events-none",
],
});
moderationBadgeSlot.hidden = true;
moderationBadgeSlot.setAttribute("aria-hidden", "true");
this.moderationBadgeSlot = moderationBadgeSlot;
// Append slot to anchor (media wrapper) instead of root to overlay thumbnail
if (this.anchorEl) {
this.anchorEl.appendChild(moderationBadgeSlot);
}
root.appendChild(anchor);
root.appendChild(moderationBadgeSlot);
root.appendChild(content);
this.applyPlaybackDatasets();
@@ -1632,18 +1646,24 @@ export class VideoCard {
let slot = this.moderationBadgeSlot;
const hiddenActive = context.activeHidden && !context.overrideActive;
if (!slot && this.root) {
if (!slot && this.anchorEl) {
slot = this.createElement("div", {
classNames: ["video-card__moderation-slot", "px-md", "pt-md"],
classNames: [
"video-card__moderation-overlay",
"absolute",
"inset-0",
"flex",
"items-center",
"justify-center",
"p-4",
"z-10",
"pointer-events-none",
],
});
slot.hidden = true;
slot.setAttribute("aria-hidden", "true");
this.moderationBadgeSlot = slot;
if (this.anchorEl && this.anchorEl.parentElement === this.root) {
this.root.insertBefore(slot, this.anchorEl.nextSibling);
} else {
this.root.appendChild(slot);
}
this.anchorEl.appendChild(slot);
}
if (badge) {
@@ -1724,6 +1744,8 @@ export class VideoCard {
if (!badge) {
const nextBadge = this.buildModerationBadge(context);
if (nextBadge && slot) {
nextBadge.style.pointerEvents = "auto"; // Enable interaction on badge itself
nextBadge.classList.add("opacity-95"); // Slight transparency
slot.appendChild(nextBadge);
slot.hidden = false;
slot.removeAttribute("aria-hidden");
@@ -1732,6 +1754,8 @@ export class VideoCard {
return;
}
badge.style.pointerEvents = "auto";
badge.classList.add("opacity-95");
badge.dataset.variant = context.overrideActive ? "neutral" : "warning";
const state = context.overrideActive
? "override"
@@ -1985,9 +2009,17 @@ export class VideoCard {
this.contentEl.setAttribute("aria-hidden", "true");
}
if (badgeSlot && this.moderationBadgeEl) {
badgeSlot.hidden = false;
badgeSlot.removeAttribute("aria-hidden");
// In hidden state (blocked), the anchor/thumbnail is hidden, so overlay slot
// won't be visible. We rely on the summary container below.
// We hide the badge slot here because we don't want it floating if anchor is hidden
// (though usually anchor hidden means children hidden too).
// However, if we move badgeSlot to be a child of anchorEl, it will be hidden automatically.
if (badgeSlot) {
// If it's a child of anchorEl, it's hidden by parent.
// We can force it hidden to be safe.
badgeSlot.hidden = true;
badgeSlot.setAttribute("aria-hidden", "true");
}
const summaryContainer = this.ensureHiddenSummaryContainer();
@@ -2008,7 +2040,11 @@ export class VideoCard {
this.contentEl.removeAttribute("hidden");
this.contentEl.removeAttribute("aria-hidden");
}
if (badgeSlot && !this.moderationBadgeEl) {
// Restore badge slot visibility if badge exists and we are not hidden
if (badgeSlot && this.moderationBadgeEl) {
badgeSlot.hidden = false;
badgeSlot.removeAttribute("aria-hidden");
} else if (badgeSlot) {
badgeSlot.hidden = true;
badgeSlot.setAttribute("aria-hidden", "true");
}
+14 -1
View File
@@ -35,7 +35,6 @@ function setupDom(t) {
"ResizeObserver",
"requestAnimationFrame",
"cancelAnimationFrame",
"navigator",
];
keys.forEach((key) => {
@@ -55,6 +54,14 @@ function setupDom(t) {
globalThis[key] = window[key];
});
// Mock navigator safely
const originalNavigator = globalThis.navigator;
Object.defineProperty(globalThis, "navigator", {
value: window.navigator,
writable: true,
configurable: true,
});
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = (cb) => setTimeout(() => cb(Date.now()), 16);
globalThis.requestAnimationFrame = window.requestAnimationFrame;
@@ -85,6 +92,12 @@ function setupDom(t) {
globalThis[key] = previousValue;
}
});
// Restore navigator
Object.defineProperty(globalThis, "navigator", {
value: originalNavigator,
writable: true,
configurable: true,
});
});
return { window, document: window.document };