Fix blurred thumbnail visuals and add moderation buttons to SimilarContentCard

- Adds `blur-xl` class to `SimilarContentCard` thumbnails when blurred, ensuring visual consistency.
- Implements `createModerationOverrideButton`, `createModerationHideButton`, and `createModerationBlockButton` in `SimilarContentCard.js` to provide missing interactivity.
- Updates `VideoModal.js` to wire up moderation callbacks (`onModerationOverride`, `onModerationHide`, `onModerationBlock`) when initializing similar content cards, dispatching global moderation events.
- Ensures moderation actions in the similar content sidebar function identically to the main video player.
This commit is contained in:
google-labs-jules[bot] committed 2026-01-16 13:34:29 +00:00
1 parent 991c090fc4
commit e6ebbb8343
4 files changed
+786 -19

No files matched your search

+307 -18
View File
@@ -1,7 +1,10 @@
import { normalizeDesignSystemContext } from "../../designSystem.js";
import { formatShortNpub } from "../../utils/formatters.js";
import { sanitizeProfileMediaUrl } from "../../utils/profileMedia.js";
import { normalizeVideoModerationContext } from "../moderationUiHelpers.js";
import {
getModerationOverrideActionLabels,
normalizeVideoModerationContext,
} from "../moderationUiHelpers.js";
import { buildModerationBadgeText } from "../moderationCopy.js";
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
@@ -99,7 +102,12 @@ export class SimilarContentCard {
? fallbackThumbnailSrc.trim()
: "";
this.callbacks = { onPlay: null };
this.callbacks = {
onPlay: null,
onModerationOverride: null,
onModerationBlock: null,
onModerationHide: null,
};
this.root = null;
this.cardStyleId = "";
@@ -117,6 +125,15 @@ export class SimilarContentCard {
this.moderationBadgeEl = null;
this.moderationBadgeLabelEl = null;
this.moderationBadgeTextEl = null;
this.moderationActionsContainer = null;
this.moderationActionButton = null;
this.moderationActionButtonMode = "";
this.moderationBlockButton = null;
this.boundShowAnywayHandler = (event) => this.handleShowAnywayClick(event);
this.boundModerationHideHandler = (event) =>
this.handleModerationHideClick(event);
this.boundModerationBlockHandler = (event) =>
this.handleModerationBlockClick(event);
this.build();
}
@@ -125,6 +142,18 @@ export class SimilarContentCard {
this.callbacks.onPlay = typeof fn === "function" ? fn : null;
}
set onModerationOverride(fn) {
this.callbacks.onModerationOverride = typeof fn === "function" ? fn : null;
}
set onModerationBlock(fn) {
this.callbacks.onModerationBlock = typeof fn === "function" ? fn : null;
}
set onModerationHide(fn) {
this.callbacks.onModerationHide = typeof fn === "function" ? fn : null;
}
getRoot() {
return this.root;
}
@@ -418,21 +447,227 @@ export class SimilarContentCard {
return normalizeVideoModerationContext(this.video?.moderation);
}
shouldShowModerationBlockButton(context = this.getModerationContext()) {
if (!context || !context.trustedMuted) {
return false;
}
if (context.activeHidden && !context.overrideActive) {
return false;
}
return true;
}
createModerationOverrideButton() {
const { text: label, ariaLabel } = getModerationOverrideActionLabels({
overrideActive: false,
});
const button = this.document.createElement("button");
button.classList.add(
"moderation-badge__action",
"flex-shrink-0",
"text-xs",
"py-1",
"px-2"
);
button.type = "button";
button.dataset.moderationAction = "override";
button.setAttribute("aria-pressed", "false");
button.setAttribute("aria-label", ariaLabel);
button.textContent = "Show";
button.addEventListener("click", this.boundShowAnywayHandler);
// Explicit pointer events since container is none
button.style.pointerEvents = "auto";
return button;
}
createModerationHideButton() {
const { text: label, ariaLabel } = getModerationOverrideActionLabels({
overrideActive: true,
});
const button = this.document.createElement("button");
button.classList.add(
"moderation-badge__action",
"flex-shrink-0",
"text-xs",
"py-1",
"px-2"
);
button.type = "button";
button.dataset.moderationAction = "hide";
button.setAttribute("aria-label", ariaLabel);
button.textContent = "Hide";
button.addEventListener("click", this.boundModerationHideHandler);
button.style.pointerEvents = "auto";
return button;
}
createModerationBlockButton() {
const button = this.document.createElement("button");
button.classList.add(
"moderation-badge__action",
"flex-shrink-0",
"text-xs",
"py-1",
"px-2"
);
button.type = "button";
button.dataset.moderationAction = "block";
button.textContent = "Block";
button.addEventListener("click", this.boundModerationBlockHandler);
button.style.pointerEvents = "auto";
return button;
}
handleShowAnywayClick(event) {
event?.preventDefault?.();
event?.stopPropagation?.();
if (this.moderationActionButton) {
this.moderationActionButton.disabled = true;
this.moderationActionButton.setAttribute("aria-busy", "true");
}
if (!this.callbacks.onModerationOverride) {
if (this.moderationActionButton) {
this.moderationActionButton.disabled = false;
this.moderationActionButton.removeAttribute("aria-busy");
}
return;
}
let result;
try {
result = this.callbacks.onModerationOverride({
event,
video: this.video,
card: this,
});
} catch (error) {
if (this.moderationActionButton) {
this.moderationActionButton.disabled = false;
this.moderationActionButton.removeAttribute("aria-busy");
}
return;
}
Promise.resolve(result).then((handled) => {
if (handled !== false) {
this.refreshModerationUi();
} else if (this.moderationActionButton) {
this.moderationActionButton.disabled = false;
this.moderationActionButton.removeAttribute("aria-busy");
}
});
}
handleModerationHideClick(event) {
event?.preventDefault?.();
event?.stopPropagation?.();
if (this.moderationActionButton) {
this.moderationActionButton.disabled = true;
this.moderationActionButton.setAttribute("aria-busy", "true");
}
if (!this.callbacks.onModerationHide) {
if (this.moderationActionButton) {
this.moderationActionButton.disabled = false;
this.moderationActionButton.removeAttribute("aria-busy");
}
return;
}
let result;
try {
result = this.callbacks.onModerationHide({
event,
video: this.video,
card: this,
});
} catch (error) {
if (this.moderationActionButton) {
this.moderationActionButton.disabled = false;
this.moderationActionButton.removeAttribute("aria-busy");
}
return;
}
Promise.resolve(result).then((handled) => {
if (handled !== false) {
this.refreshModerationUi();
} else if (this.moderationActionButton) {
this.moderationActionButton.disabled = false;
this.moderationActionButton.removeAttribute("aria-busy");
}
});
}
handleModerationBlockClick(event) {
event?.preventDefault?.();
event?.stopPropagation?.();
if (this.moderationBlockButton) {
this.moderationBlockButton.disabled = true;
this.moderationBlockButton.setAttribute("aria-busy", "true");
}
if (!this.callbacks.onModerationBlock) {
if (this.moderationBlockButton) {
this.moderationBlockButton.disabled = false;
this.moderationBlockButton.removeAttribute("aria-busy");
}
return;
}
let result;
try {
result = this.callbacks.onModerationBlock({
event,
video: this.video,
card: this,
});
} catch (error) {
if (this.moderationBlockButton) {
this.moderationBlockButton.disabled = false;
this.moderationBlockButton.removeAttribute("aria-busy");
}
return;
}
Promise.resolve(result).then((handled) => {
if (handled !== false) {
this.refreshModerationUi();
} else if (this.moderationBlockButton) {
this.moderationBlockButton.disabled = false;
this.moderationBlockButton.removeAttribute("aria-busy");
}
});
}
buildModerationBadge(context = this.getModerationContext()) {
if (!context.shouldShow || !context.trustedMuted) {
this.moderationBadgeEl = null;
this.moderationActionsContainer = null;
this.moderationActionButton = null;
this.moderationActionButtonMode = "";
this.moderationBlockButton = null;
return null;
}
const badge = this.document.createElement("div");
badge.className = "moderation-badge moderation-badge--interactive opacity-95";
badge.dataset.variant = "warning";
badge.dataset.variant = context.overrideActive ? "neutral" : "warning";
badge.dataset.moderationBadge = "true";
badge.dataset.moderationState = "trusted-mute";
badge.dataset.moderationState = context.overrideActive
? "override"
: "trusted-mute";
// Allow interaction with children
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");
@@ -444,7 +679,13 @@ export class SimilarContentCard {
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");
if (context.overrideActive) {
path.setAttribute("d", "M10 18a8 8 0 100-16 8 8 0 000 16zm3.78-9.72a.75.75 0 00-1.06-1.06L9 11.94l-1.72-1.72a.75.75 0 10-1.06 1.06l2.25 2.25a.75.75 0 001.06 0l3.25-3.25z");
} else {
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");
@@ -453,13 +694,40 @@ export class SimilarContentCard {
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";
text.className = "moderation-badge__text text-xs";
text.textContent = "Blocked by trusted";
label.appendChild(text);
badge.appendChild(label);
const actions = this.document.createElement("div");
actions.className = "moderation-badge__actions flex items-center gap-1 mt-1";
if (context.allowOverride) {
if (context.overrideActive) {
const hideButton = this.createModerationHideButton();
actions.appendChild(hideButton);
this.moderationActionButton = hideButton;
this.moderationActionButtonMode = "hide";
} else {
const showButton = this.createModerationOverrideButton();
actions.appendChild(showButton);
this.moderationActionButton = showButton;
this.moderationActionButtonMode = "override";
}
}
if (this.shouldShowModerationBlockButton(context)) {
const blockButton = this.createModerationBlockButton();
actions.appendChild(blockButton);
this.moderationBlockButton = blockButton;
}
if (actions.childElementCount > 0) {
badge.appendChild(actions);
this.moderationActionsContainer = actions;
}
this.moderationBadgeEl = badge;
return badge;
}
@@ -471,24 +739,39 @@ export class SimilarContentCard {
this.moderationBadgeEl.remove();
this.moderationBadgeEl = null;
}
this.moderationActionsContainer = null;
this.moderationActionButton = null;
this.moderationActionButtonMode = "";
this.moderationBlockButton = null;
// Apply blurring class if needed
if (this.thumbnailEl) {
const shouldBlur =
(this.shouldMaskNsfwForOwner ||
this.video?.moderation?.blurThumbnail) &&
!context.overrideActive;
if (shouldBlur) {
this.thumbnailEl.dataset.thumbnailState = "blurred";
this.thumbnailEl.classList.add("blur-xl");
} else {
delete this.thumbnailEl.dataset.thumbnailState;
this.thumbnailEl.classList.remove("blur-xl");
}
}
// Check specifically for muted/blurred state where we want the overlay
if (context.shouldShow && context.trustedMuted && !context.overrideActive) {
// Ensure media link exists
if (context.shouldShow && context.trustedMuted) {
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.className =
"absolute inset-0 flex flex-col 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;
}
}
@@ -502,7 +785,12 @@ export class SimilarContentCard {
img.dataset.videoThumbnail = "true";
// Modified: Add transition classes for hover zoom
img.classList.add("transition-transform", "duration-300", "ease-out", "group-hover:scale-105");
img.classList.add(
"transition-transform",
"duration-300",
"ease-out",
"group-hover:scale-105"
);
const rawThumbnail =
typeof this.video.thumbnail === "string"
@@ -537,6 +825,7 @@ export class SimilarContentCard {
if (this.shouldMaskNsfwForOwner || this.video?.moderation?.blurThumbnail) {
img.dataset.thumbnailState = "blurred";
img.classList.add("blur-xl");
}
const handleLoad = () => {
+16
View File
@@ -4947,6 +4947,22 @@ export class VideoModal {
cardIndex
);
// Wire up moderation callbacks
if (card) {
card.onModerationOverride = (detail) => {
this.dispatch("video:moderation-override", detail);
return true;
};
card.onModerationHide = (detail) => {
this.dispatch("video:moderation-hide", detail);
return true;
};
card.onModerationBlock = (detail) => {
this.dispatch("video:moderation-block", detail);
return true;
};
}
const root = card.getRoot();
if (!root) {
return;