mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 04:42:57 +00:00
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:
1 parent
991c090fc4
commit
e6ebbb8343
4 files changed
+786
-19
No files matched your search
@@ -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 = () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user