mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-31 09:52:24 +00:00
Merge pull request #906 from PR0M3TH3AN/codex/implement-moderation-controls-in-history-view
Add moderation UI support to watch history cards
This commit is contained in:
6 files changed
+1126
-192
No files matched your search
+20
-1
@@ -3245,6 +3245,13 @@
|
||||
@apply h-full w-full object-cover;
|
||||
}
|
||||
|
||||
.watch-history-card [data-thumbnail-state="blurred"] img,
|
||||
.watch-history-card [data-visual-state="blurred"] img {
|
||||
@apply transition duration-base ease-in-out;
|
||||
filter: blur(var(--blur-thumbnail-blur));
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.watch-history-card__details {
|
||||
@apply flex min-w-0 flex-1 flex-col justify-center gap-sm;
|
||||
}
|
||||
@@ -3279,7 +3286,7 @@
|
||||
}
|
||||
|
||||
.watch-history-card__creatorAvatar {
|
||||
@apply flex h-10 w-10 items-center justify-center rounded-full bg-panel transition duration-base ease-out hover:ring-2 hover:ring-opacity-50 focus-visible:ring-2 focus-visible:ring-opacity-50;
|
||||
@apply flex h-10 w-10 items-center justify-center overflow-hidden rounded-full bg-panel transition duration-base ease-out hover:ring-2 hover:ring-opacity-50 focus-visible:ring-2 focus-visible:ring-opacity-50;
|
||||
}
|
||||
|
||||
.watch-history-card__creatorAvatar:hover,
|
||||
@@ -3310,6 +3317,18 @@
|
||||
.watch-history-card__action--danger {
|
||||
@apply bg-critical text-white hover:bg-critical-strong focus-visible:bg-critical-strong;
|
||||
}
|
||||
|
||||
.watch-history-card__moderation {
|
||||
@apply mt-sm flex flex-wrap items-center gap-sm;
|
||||
}
|
||||
|
||||
.watch-history-card__hidden {
|
||||
@apply mt-md rounded-lg border border-dashed border-border bg-panel/70 text-sm text-text;
|
||||
}
|
||||
|
||||
.watch-history-card__hidden .moderation-badge {
|
||||
@apply self-start;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
|
||||
@@ -12,6 +12,11 @@ import {
|
||||
} from "./config.js";
|
||||
import { getApplication } from "./applicationContext.js";
|
||||
import { userLogger } from "./utils/logger.js";
|
||||
import {
|
||||
normalizeVideoModerationContext,
|
||||
applyModerationContextDatasets,
|
||||
} from "./ui/moderationUiHelpers.js";
|
||||
import { buildModerationBadgeText } from "./ui/moderationCopy.js";
|
||||
|
||||
export const WATCH_HISTORY_EMPTY_COPY =
|
||||
"Your watch history is empty. Watch some videos to populate this list.";
|
||||
@@ -351,6 +356,624 @@ function getPointerVideoId(video, pointer) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
||||
const historyCardRegistry = new Map();
|
||||
|
||||
function sanitizeIdToken(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
const token = value.trim().replace(/[^a-zA-Z0-9_-]/g, "");
|
||||
return token;
|
||||
}
|
||||
|
||||
function getHistoryCardModerationId(pointerKey) {
|
||||
const token = sanitizeIdToken(pointerKey);
|
||||
return token ? `watch-history-${token}-moderation` : "";
|
||||
}
|
||||
|
||||
function getHistoryCardModerationState(context) {
|
||||
if (!context) {
|
||||
return "";
|
||||
}
|
||||
if (context.overrideActive) {
|
||||
return "override";
|
||||
}
|
||||
if (context.activeHidden && !context.overrideActive) {
|
||||
return "hidden";
|
||||
}
|
||||
if (context.trustedMuted) {
|
||||
return "trusted-mute";
|
||||
}
|
||||
return "blocked";
|
||||
}
|
||||
|
||||
function shouldShowHistoryCardHideButton(context) {
|
||||
if (!context || !context.trustedMuted) {
|
||||
return false;
|
||||
}
|
||||
if (context.activeHidden && !context.overrideActive) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getModerationBadgeIconShape(state) {
|
||||
if (state === "override") {
|
||||
return {
|
||||
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",
|
||||
fillRule: "evenodd",
|
||||
clipRule: "evenodd"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
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",
|
||||
fillRule: "evenodd",
|
||||
clipRule: "evenodd"
|
||||
};
|
||||
}
|
||||
|
||||
function updateHistoryCardBadgeIcon(svg, state) {
|
||||
if (!svg) {
|
||||
return;
|
||||
}
|
||||
svg.dataset.iconState = state;
|
||||
const path = svg.firstElementChild;
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
const { d, fillRule, clipRule } = getModerationBadgeIconShape(state);
|
||||
path.setAttribute("d", d);
|
||||
if (fillRule) {
|
||||
path.setAttribute("fill-rule", fillRule);
|
||||
} else {
|
||||
path.removeAttribute("fill-rule");
|
||||
}
|
||||
if (clipRule) {
|
||||
path.setAttribute("clip-rule", clipRule);
|
||||
} else {
|
||||
path.removeAttribute("clip-rule");
|
||||
}
|
||||
}
|
||||
|
||||
function createHistoryCardBadgeIcon(doc, state) {
|
||||
const wrapper = doc.createElement("span");
|
||||
wrapper.className = "moderation-badge__icon";
|
||||
wrapper.setAttribute("aria-hidden", "true");
|
||||
const svg = doc.createElementNS(SVG_NAMESPACE, "svg");
|
||||
svg.setAttribute("viewBox", "0 0 20 20");
|
||||
svg.setAttribute("focusable", "false");
|
||||
svg.setAttribute("aria-hidden", "true");
|
||||
svg.classList.add("moderation-badge__icon-mark");
|
||||
const path = doc.createElementNS(SVG_NAMESPACE, "path");
|
||||
path.setAttribute("fill", "currentColor");
|
||||
svg.appendChild(path);
|
||||
wrapper.appendChild(svg);
|
||||
updateHistoryCardBadgeIcon(svg, state);
|
||||
return { wrapper, svg };
|
||||
}
|
||||
|
||||
function cleanupHistoryCard(pointerKey) {
|
||||
if (!pointerKey || !historyCardRegistry.has(pointerKey)) {
|
||||
return;
|
||||
}
|
||||
const ref = historyCardRegistry.get(pointerKey);
|
||||
historyCardRegistry.delete(pointerKey);
|
||||
if (!ref || typeof ref !== "object") {
|
||||
return;
|
||||
}
|
||||
if (ref.overrideButton && typeof ref.overrideButton.removeEventListener === "function" && typeof ref.boundOverride === "function") {
|
||||
ref.overrideButton.removeEventListener("click", ref.boundOverride);
|
||||
}
|
||||
if (ref.hideButton && typeof ref.hideButton.removeEventListener === "function" && typeof ref.boundHide === "function") {
|
||||
ref.hideButton.removeEventListener("click", ref.boundHide);
|
||||
}
|
||||
if (ref.badgeEl && ref.badgeEl.parentNode) {
|
||||
ref.badgeEl.parentNode.removeChild(ref.badgeEl);
|
||||
}
|
||||
if (ref.hiddenContainer && ref.hiddenContainer.parentNode) {
|
||||
ref.hiddenContainer.parentNode.removeChild(ref.hiddenContainer);
|
||||
}
|
||||
}
|
||||
|
||||
function registerHistoryCard(pointerKey, ref) {
|
||||
if (!pointerKey || !ref) {
|
||||
return;
|
||||
}
|
||||
cleanupHistoryCard(pointerKey);
|
||||
historyCardRegistry.set(pointerKey, ref);
|
||||
}
|
||||
|
||||
function clearHistoryCardRegistry() {
|
||||
const keys = Array.from(historyCardRegistry.keys());
|
||||
keys.forEach((key) => {
|
||||
cleanupHistoryCard(key);
|
||||
});
|
||||
historyCardRegistry.clear();
|
||||
}
|
||||
|
||||
function ensureHiddenSummaryContainer(ref) {
|
||||
const article = ref?.article;
|
||||
if (!article || typeof article.insertBefore !== "function") {
|
||||
return null;
|
||||
}
|
||||
let container = ref.hiddenContainer;
|
||||
const doc = article.ownerDocument || document;
|
||||
if (!container) {
|
||||
container = doc.createElement("div");
|
||||
container.className = "watch-history-card__hidden bv-stack bv-stack--tight p-md";
|
||||
container.dataset.moderationHiddenContainer = "true";
|
||||
container.setAttribute("role", "group");
|
||||
container.setAttribute("aria-live", "polite");
|
||||
ref.hiddenContainer = container;
|
||||
}
|
||||
container.hidden = false;
|
||||
container.removeAttribute("aria-hidden");
|
||||
if (container.parentNode !== article) {
|
||||
article.insertBefore(container, article.firstChild || null);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
function updateHistoryCardHiddenState(ref, context) {
|
||||
const hiddenActive = Boolean(context?.activeHidden && !context?.overrideActive);
|
||||
const primary = ref.primary;
|
||||
const meta = ref.meta;
|
||||
|
||||
if (hiddenActive) {
|
||||
if (primary && typeof primary.setAttribute === "function") {
|
||||
primary.setAttribute("hidden", "");
|
||||
primary.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
if (meta && typeof meta.setAttribute === "function") {
|
||||
meta.setAttribute("hidden", "");
|
||||
meta.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
const container = ensureHiddenSummaryContainer(ref);
|
||||
if (container) {
|
||||
const description = buildModerationBadgeText(context, { variant: "card" });
|
||||
if (description) {
|
||||
container.setAttribute("aria-label", description);
|
||||
} else {
|
||||
container.removeAttribute("aria-label");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (primary && typeof primary.removeAttribute === "function") {
|
||||
primary.removeAttribute("hidden");
|
||||
primary.removeAttribute("aria-hidden");
|
||||
}
|
||||
if (meta && typeof meta.removeAttribute === "function") {
|
||||
meta.removeAttribute("hidden");
|
||||
meta.removeAttribute("aria-hidden");
|
||||
}
|
||||
const container = ref.hiddenContainer;
|
||||
if (container) {
|
||||
container.hidden = true;
|
||||
container.setAttribute("aria-hidden", "true");
|
||||
container.removeAttribute("aria-label");
|
||||
container.removeAttribute("aria-labelledby");
|
||||
if (container.parentNode) {
|
||||
container.parentNode.removeChild(container);
|
||||
}
|
||||
ref.hiddenContainer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createHistoryCardBadge(ref, context) {
|
||||
const article = ref?.article;
|
||||
const doc = (article && article.ownerDocument) || (typeof document !== "undefined" ? document : null);
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
const badge = doc.createElement("div");
|
||||
badge.className = "moderation-badge flex flex-wrap items-center gap-sm";
|
||||
badge.dataset.moderationBadge = "true";
|
||||
const { wrapper, svg } = createHistoryCardBadgeIcon(doc, getHistoryCardModerationState(context));
|
||||
badge.appendChild(wrapper);
|
||||
const text = doc.createElement("span");
|
||||
text.className = "moderation-badge__text whitespace-nowrap";
|
||||
badge.appendChild(text);
|
||||
ref.badgeEl = badge;
|
||||
ref.badgeTextEl = text;
|
||||
ref.badgeIconSvg = svg;
|
||||
ref.badgeIconWrapper = wrapper;
|
||||
if (typeof ref.boundOverride !== "function") {
|
||||
ref.boundOverride = (event) => handleHistoryCardModerationOverride(event, ref);
|
||||
}
|
||||
if (typeof ref.boundHide !== "function") {
|
||||
ref.boundHide = (event) => handleHistoryCardModerationHide(event, ref);
|
||||
}
|
||||
if (!ref.overrideButton) {
|
||||
const overrideButton = doc.createElement("button");
|
||||
overrideButton.type = "button";
|
||||
overrideButton.className = "moderation-badge__action flex-shrink-0";
|
||||
overrideButton.dataset.moderationAction = "override";
|
||||
overrideButton.textContent = "Show anyway";
|
||||
overrideButton.addEventListener("click", ref.boundOverride);
|
||||
ref.overrideButton = overrideButton;
|
||||
}
|
||||
if (!ref.hideButton) {
|
||||
const hideButton = doc.createElement("button");
|
||||
hideButton.type = "button";
|
||||
hideButton.className = "moderation-badge__action flex-shrink-0";
|
||||
hideButton.dataset.moderationAction = "hide";
|
||||
hideButton.textContent = "Hide";
|
||||
hideButton.addEventListener("click", ref.boundHide);
|
||||
ref.hideButton = hideButton;
|
||||
}
|
||||
return badge;
|
||||
}
|
||||
|
||||
function updateHistoryCardBadge(ref, context) {
|
||||
const hiddenActive = Boolean(context?.activeHidden && !context?.overrideActive);
|
||||
if (!context?.shouldShow) {
|
||||
if (ref.overrideButton && typeof ref.overrideButton.removeEventListener === "function" && typeof ref.boundOverride === "function") {
|
||||
ref.overrideButton.removeEventListener("click", ref.boundOverride);
|
||||
}
|
||||
if (ref.hideButton && typeof ref.hideButton.removeEventListener === "function" && typeof ref.boundHide === "function") {
|
||||
ref.hideButton.removeEventListener("click", ref.boundHide);
|
||||
}
|
||||
if (ref.badgeEl && ref.badgeEl.parentNode) {
|
||||
ref.badgeEl.parentNode.removeChild(ref.badgeEl);
|
||||
}
|
||||
ref.badgeEl = null;
|
||||
ref.badgeTextEl = null;
|
||||
ref.badgeIconSvg = null;
|
||||
ref.badgeIconWrapper = null;
|
||||
ref.overrideButton = null;
|
||||
ref.hideButton = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ref.badgeEl) {
|
||||
createHistoryCardBadge(ref, context);
|
||||
}
|
||||
|
||||
const badge = ref.badgeEl;
|
||||
if (!badge) {
|
||||
return;
|
||||
}
|
||||
|
||||
badge.dataset.variant = context.overrideActive ? "neutral" : "warning";
|
||||
const state = context.overrideActive ? "override" : getHistoryCardModerationState(context);
|
||||
badge.dataset.moderationState = state;
|
||||
if (hiddenActive && context.effectiveHideReason) {
|
||||
badge.dataset.moderationHideReason = context.effectiveHideReason;
|
||||
} else if (badge.dataset.moderationHideReason) {
|
||||
delete badge.dataset.moderationHideReason;
|
||||
}
|
||||
|
||||
if (ref.badgeIconSvg) {
|
||||
updateHistoryCardBadgeIcon(ref.badgeIconSvg, state);
|
||||
}
|
||||
|
||||
const textContent = buildModerationBadgeText(context, { variant: "card" });
|
||||
if (ref.badgeTextEl) {
|
||||
ref.badgeTextEl.textContent = textContent;
|
||||
}
|
||||
|
||||
const muteNames = Array.isArray(context.trustedMuteDisplayNames)
|
||||
? context.trustedMuteDisplayNames
|
||||
.map((name) => (typeof name === "string" ? name.trim() : ""))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
const reporterNames = Array.isArray(context.reporterDisplayNames)
|
||||
? context.reporterDisplayNames
|
||||
.map((name) => (typeof name === "string" ? name.trim() : ""))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
|
||||
const allNames = [...muteNames, ...reporterNames];
|
||||
const uniqueNames = [];
|
||||
const seen = new Set();
|
||||
for (const name of allNames) {
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const key = name.toLowerCase();
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
uniqueNames.push(name);
|
||||
}
|
||||
|
||||
if (uniqueNames.length) {
|
||||
const hasMuted = muteNames.length > 0;
|
||||
const hasReporters = reporterNames.length > 0;
|
||||
const prefix = hasMuted && hasReporters
|
||||
? "Muted/Reported by"
|
||||
: hasMuted
|
||||
? "Muted by"
|
||||
: "Reported by";
|
||||
const joined = uniqueNames.join(", ");
|
||||
badge.title = `${prefix} ${joined}`;
|
||||
badge.setAttribute("aria-label", `${textContent}. ${prefix} ${joined}.`);
|
||||
} else if (textContent) {
|
||||
badge.removeAttribute("title");
|
||||
badge.setAttribute("aria-label", `${textContent}.`);
|
||||
} else {
|
||||
badge.removeAttribute("title");
|
||||
badge.removeAttribute("aria-label");
|
||||
}
|
||||
|
||||
const parent = hiddenActive ? ensureHiddenSummaryContainer(ref) : ref.badgeMount;
|
||||
if (parent && badge.parentNode !== parent) {
|
||||
if (badge.parentNode) {
|
||||
badge.parentNode.removeChild(badge);
|
||||
}
|
||||
parent.appendChild(badge);
|
||||
}
|
||||
|
||||
if (!context.overrideActive && context.allowOverride && ref.overrideButton) {
|
||||
if (ref.overrideButton.parentNode !== badge) {
|
||||
badge.appendChild(ref.overrideButton);
|
||||
}
|
||||
ref.overrideButton.disabled = false;
|
||||
ref.overrideButton.removeAttribute("aria-busy");
|
||||
} else if (ref.overrideButton && ref.overrideButton.parentNode === badge) {
|
||||
badge.removeChild(ref.overrideButton);
|
||||
}
|
||||
|
||||
if (shouldShowHistoryCardHideButton(context) && ref.hideButton) {
|
||||
if (ref.hideButton.parentNode !== badge) {
|
||||
badge.appendChild(ref.hideButton);
|
||||
}
|
||||
ref.hideButton.disabled = false;
|
||||
ref.hideButton.removeAttribute("aria-busy");
|
||||
} else if (ref.hideButton && ref.hideButton.parentNode === badge) {
|
||||
badge.removeChild(ref.hideButton);
|
||||
}
|
||||
}
|
||||
|
||||
function updateHistoryCardAria(ref) {
|
||||
const badge = ref.badgeEl;
|
||||
const badgeId = badge ? getHistoryCardModerationId(ref.pointerKey) : "";
|
||||
if (badge) {
|
||||
if (badgeId) {
|
||||
badge.id = badgeId;
|
||||
} else {
|
||||
badge.removeAttribute("id");
|
||||
}
|
||||
}
|
||||
|
||||
const targets = [
|
||||
ref.thumbnailLink,
|
||||
ref.titleLink,
|
||||
ref.playButton,
|
||||
ref.channelButton,
|
||||
ref.creatorNameButton,
|
||||
ref.avatarButton
|
||||
].filter((el) => el && typeof el.getAttribute === "function");
|
||||
|
||||
targets.forEach((el) => {
|
||||
const existing = el.getAttribute("aria-describedby") || "";
|
||||
const tokens = existing.split(/\s+/).filter(Boolean);
|
||||
if (ref.badgeId) {
|
||||
for (let index = tokens.length - 1; index >= 0; index -= 1) {
|
||||
if (tokens[index] === ref.badgeId) {
|
||||
tokens.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (badgeId) {
|
||||
tokens.push(badgeId);
|
||||
}
|
||||
if (tokens.length) {
|
||||
el.setAttribute("aria-describedby", Array.from(new Set(tokens)).join(" "));
|
||||
} else {
|
||||
el.removeAttribute("aria-describedby");
|
||||
}
|
||||
});
|
||||
|
||||
if (ref.overrideButton) {
|
||||
if (badgeId) {
|
||||
ref.overrideButton.setAttribute("aria-describedby", badgeId);
|
||||
} else {
|
||||
ref.overrideButton.removeAttribute("aria-describedby");
|
||||
}
|
||||
}
|
||||
|
||||
if (ref.hideButton) {
|
||||
if (badgeId) {
|
||||
ref.hideButton.setAttribute("aria-describedby", badgeId);
|
||||
} else {
|
||||
ref.hideButton.removeAttribute("aria-describedby");
|
||||
}
|
||||
}
|
||||
|
||||
if (ref.hiddenContainer) {
|
||||
if (badgeId) {
|
||||
ref.hiddenContainer.setAttribute("aria-labelledby", badgeId);
|
||||
} else {
|
||||
ref.hiddenContainer.removeAttribute("aria-labelledby");
|
||||
}
|
||||
}
|
||||
|
||||
ref.badgeId = badgeId;
|
||||
}
|
||||
|
||||
function applyHistoryCardModeration(ref, context) {
|
||||
if (!ref) {
|
||||
return;
|
||||
}
|
||||
const normalizedContext = context || normalizeVideoModerationContext(ref.video?.moderation);
|
||||
applyModerationContextDatasets(normalizedContext, {
|
||||
root: ref.article,
|
||||
thumbnail: ref.thumbnailInner || ref.thumbnailLink,
|
||||
avatar: ref.avatarButton
|
||||
});
|
||||
updateHistoryCardHiddenState(ref, normalizedContext);
|
||||
updateHistoryCardBadge(ref, normalizedContext);
|
||||
updateHistoryCardAria(ref);
|
||||
}
|
||||
|
||||
function handleHistoryCardModerationOverride(event, ref) {
|
||||
if (event) {
|
||||
if (typeof event.preventDefault === "function") {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (typeof event.stopPropagation === "function") {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
const button = ref.overrideButton;
|
||||
if (button) {
|
||||
button.disabled = true;
|
||||
button.setAttribute("aria-busy", "true");
|
||||
}
|
||||
|
||||
const video = ref.video;
|
||||
if (!video || typeof video !== "object" || !video.id) {
|
||||
if (button) {
|
||||
button.disabled = false;
|
||||
button.removeAttribute("aria-busy");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const app = getAppInstance();
|
||||
let result;
|
||||
try {
|
||||
if (typeof app?.handleModerationOverride === "function") {
|
||||
result = app.handleModerationOverride({ video });
|
||||
} else {
|
||||
const doc = (ref.article && ref.article.ownerDocument) || (typeof document !== "undefined" ? document : null);
|
||||
if (doc && typeof doc.dispatchEvent === "function") {
|
||||
doc.dispatchEvent(
|
||||
new CustomEvent("video:moderation-override", { detail: { video } })
|
||||
);
|
||||
}
|
||||
result = true;
|
||||
}
|
||||
} catch (error) {
|
||||
if (button) {
|
||||
button.disabled = false;
|
||||
button.removeAttribute("aria-busy");
|
||||
}
|
||||
if (isDevEnv) {
|
||||
userLogger.warn("[historyView] Moderation override handler threw:", error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.resolve(result)
|
||||
.then((handled) => {
|
||||
if (handled === false) {
|
||||
if (button) {
|
||||
button.disabled = false;
|
||||
button.removeAttribute("aria-busy");
|
||||
}
|
||||
return;
|
||||
}
|
||||
const context = normalizeVideoModerationContext(ref.video?.moderation);
|
||||
applyHistoryCardModeration(ref, context);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (isDevEnv) {
|
||||
userLogger.warn("[historyView] Moderation override failed:", error);
|
||||
}
|
||||
if (button) {
|
||||
button.disabled = false;
|
||||
button.removeAttribute("aria-busy");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleHistoryCardModerationHide(event, ref) {
|
||||
if (event) {
|
||||
if (typeof event.preventDefault === "function") {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (typeof event.stopPropagation === "function") {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
const button = ref.hideButton;
|
||||
if (button) {
|
||||
button.disabled = true;
|
||||
button.setAttribute("aria-busy", "true");
|
||||
}
|
||||
|
||||
const video = ref.video;
|
||||
if (!video || typeof video !== "object" || !video.id) {
|
||||
if (button) {
|
||||
button.disabled = false;
|
||||
button.removeAttribute("aria-busy");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const app = getAppInstance();
|
||||
let result;
|
||||
try {
|
||||
if (typeof app?.handleModerationHide === "function") {
|
||||
result = app.handleModerationHide({ video });
|
||||
} else {
|
||||
const doc = (ref.article && ref.article.ownerDocument) || (typeof document !== "undefined" ? document : null);
|
||||
if (doc && typeof doc.dispatchEvent === "function") {
|
||||
doc.dispatchEvent(
|
||||
new CustomEvent("video:moderation-hide", { detail: { video } })
|
||||
);
|
||||
}
|
||||
result = true;
|
||||
}
|
||||
} catch (error) {
|
||||
if (button) {
|
||||
button.disabled = false;
|
||||
button.removeAttribute("aria-busy");
|
||||
}
|
||||
if (isDevEnv) {
|
||||
userLogger.warn("[historyView] Moderation hide handler threw:", error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.resolve(result)
|
||||
.then((handled) => {
|
||||
if (handled === false) {
|
||||
if (button) {
|
||||
button.disabled = false;
|
||||
button.removeAttribute("aria-busy");
|
||||
}
|
||||
return;
|
||||
}
|
||||
const context = normalizeVideoModerationContext(ref.video?.moderation);
|
||||
applyHistoryCardModeration(ref, context);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (isDevEnv) {
|
||||
userLogger.warn("[historyView] Moderation hide failed:", error);
|
||||
}
|
||||
if (button) {
|
||||
button.disabled = false;
|
||||
button.removeAttribute("aria-busy");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function refreshRegisteredHistoryCards(video, context, pointerKeys = null) {
|
||||
const filterSet = Array.isArray(pointerKeys) && pointerKeys.length ? new Set(pointerKeys) : null;
|
||||
const videoId = video && typeof video.id === "string" ? video.id : "";
|
||||
historyCardRegistry.forEach((ref, pointerKey) => {
|
||||
if (!ref) {
|
||||
return;
|
||||
}
|
||||
if (filterSet && !filterSet.has(pointerKey)) {
|
||||
return;
|
||||
}
|
||||
if (videoId) {
|
||||
ref.video = video;
|
||||
}
|
||||
applyHistoryCardModeration(ref, context);
|
||||
});
|
||||
}
|
||||
|
||||
export function buildHistoryCard({ item, video, profile, metadataPreference }) {
|
||||
const article = document.createElement("article");
|
||||
article.className = "watch-history-card";
|
||||
@@ -438,6 +1061,9 @@ export function buildHistoryCard({ item, video, profile, metadataPreference }) {
|
||||
|
||||
details.appendChild(titleLink);
|
||||
details.appendChild(created);
|
||||
const moderationMount = document.createElement("div");
|
||||
moderationMount.className = "watch-history-card__moderation flex flex-wrap items-center gap-sm";
|
||||
details.appendChild(moderationMount);
|
||||
|
||||
primary.appendChild(thumbnailLink);
|
||||
primary.appendChild(details);
|
||||
@@ -569,6 +1195,42 @@ export function buildHistoryCard({ item, video, profile, metadataPreference }) {
|
||||
article.appendChild(primary);
|
||||
article.appendChild(meta);
|
||||
|
||||
if (pointerVideoId) {
|
||||
article.dataset.videoId = pointerVideoId;
|
||||
}
|
||||
|
||||
const moderationContext = normalizeVideoModerationContext(video?.moderation);
|
||||
const cardRef = {
|
||||
pointerKey: item.pointerKey,
|
||||
article,
|
||||
primary,
|
||||
meta,
|
||||
details,
|
||||
badgeMount: moderationMount,
|
||||
thumbnailInner,
|
||||
thumbnailLink,
|
||||
titleLink,
|
||||
playButton,
|
||||
channelButton,
|
||||
removeButton,
|
||||
creatorNameButton,
|
||||
avatarButton: creatorAvatarButton,
|
||||
video: video || null,
|
||||
hiddenContainer: null,
|
||||
badgeEl: null,
|
||||
badgeTextEl: null,
|
||||
badgeIconSvg: null,
|
||||
badgeIconWrapper: null,
|
||||
overrideButton: null,
|
||||
hideButton: null,
|
||||
badgeId: "",
|
||||
boundOverride: null,
|
||||
boundHide: null
|
||||
};
|
||||
|
||||
registerHistoryCard(item.pointerKey, cardRef);
|
||||
applyHistoryCardModeration(cardRef, moderationContext);
|
||||
|
||||
return article;
|
||||
}
|
||||
|
||||
@@ -974,6 +1636,109 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleModerationUpdate(video) {
|
||||
if (!video || typeof video !== "object" || !video.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let workingVideo = video;
|
||||
if (typeof structuredClone === "function") {
|
||||
try {
|
||||
workingVideo = structuredClone(video);
|
||||
} catch (error) {
|
||||
workingVideo = video;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
workingVideo = JSON.parse(JSON.stringify(video));
|
||||
} catch (error) {
|
||||
workingVideo = { ...video };
|
||||
if (video.moderation && typeof video.moderation === "object") {
|
||||
workingVideo.moderation = { ...video.moderation };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const app = getAppInstance();
|
||||
if (typeof app?.decorateVideoModeration === "function") {
|
||||
try {
|
||||
const decorated = app.decorateVideoModeration(workingVideo);
|
||||
if (decorated) {
|
||||
workingVideo = decorated;
|
||||
}
|
||||
} catch (error) {
|
||||
if (isDevEnv) {
|
||||
userLogger.warn(
|
||||
"[historyView] Failed to decorate moderation update:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const context = normalizeVideoModerationContext(workingVideo?.moderation);
|
||||
const affectedPointers = [];
|
||||
state.items.forEach((entry) => {
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
const entryVideo = entry.metadata?.video || entry.video;
|
||||
if (entryVideo && entryVideo.id === workingVideo.id) {
|
||||
entry.metadata.video = workingVideo;
|
||||
entry.video = workingVideo;
|
||||
if (entry.pointerKey) {
|
||||
const cached = state.metadataCache.get(entry.pointerKey) || {};
|
||||
state.metadataCache.set(entry.pointerKey, {
|
||||
...cached,
|
||||
video: workingVideo
|
||||
});
|
||||
if (state.metadataStorageEnabled) {
|
||||
try {
|
||||
watchHistoryService.setLocalMetadata?.(entry.pointerKey, {
|
||||
...cached,
|
||||
video: workingVideo
|
||||
});
|
||||
} catch (error) {
|
||||
if (isDevEnv) {
|
||||
userLogger.warn(
|
||||
"[historyView] Failed to persist moderation metadata for pointer:",
|
||||
entry.pointerKey,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
affectedPointers.push(entry.pointerKey);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
refreshRegisteredHistoryCards(
|
||||
workingVideo,
|
||||
context,
|
||||
affectedPointers.length ? affectedPointers : null
|
||||
);
|
||||
}
|
||||
|
||||
function subscribeToModerationEvents() {
|
||||
if (typeof document === "undefined" || typeof document.addEventListener !== "function") {
|
||||
return;
|
||||
}
|
||||
const handler = (event) => {
|
||||
const video = event?.detail?.video;
|
||||
if (!video) {
|
||||
return;
|
||||
}
|
||||
handleModerationUpdate(video);
|
||||
};
|
||||
document.addEventListener("video:moderation-override", handler);
|
||||
document.addEventListener("video:moderation-hide", handler);
|
||||
subscriptions.add(() => {
|
||||
document.removeEventListener("video:moderation-override", handler);
|
||||
document.removeEventListener("video:moderation-hide", handler);
|
||||
});
|
||||
}
|
||||
|
||||
function setLoadingState(loading) {
|
||||
state.isLoading = loading;
|
||||
if (elements.loading) {
|
||||
@@ -1005,6 +1770,7 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
elements.grid.innerHTML = "";
|
||||
setHidden(elements.grid, true);
|
||||
}
|
||||
clearHistoryCardRegistry();
|
||||
if (elements.empty) {
|
||||
setTextContent(elements.empty, emptyCopy);
|
||||
setHidden(elements.empty, false);
|
||||
@@ -1218,6 +1984,27 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
profile: profile || null
|
||||
};
|
||||
|
||||
if (metadata.video && typeof metadata.video === "object") {
|
||||
const app = getAppInstance();
|
||||
if (typeof app?.decorateVideoModeration === "function") {
|
||||
try {
|
||||
const decorated = app.decorateVideoModeration(metadata.video);
|
||||
if (decorated) {
|
||||
metadata.video = decorated;
|
||||
}
|
||||
} catch (error) {
|
||||
if (isDevEnv) {
|
||||
userLogger.warn(
|
||||
"[historyView] Failed to decorate video moderation:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
video = metadata.video || null;
|
||||
|
||||
if (pointerKeyValue) {
|
||||
state.metadataCache.set(pointerKeyValue, {
|
||||
video: metadata.video,
|
||||
@@ -1726,6 +2513,7 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
state.cursor = 0;
|
||||
state.hasMore = state.items.length > 0;
|
||||
elements.grid.innerHTML = "";
|
||||
clearHistoryCardRegistry();
|
||||
removeEmptyDayContainers(elements.grid);
|
||||
if (!state.items.length) {
|
||||
showEmptyState();
|
||||
@@ -1754,6 +2542,7 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
subscribeToMetadataPreference();
|
||||
subscribeToRepublishEvents();
|
||||
subscribeToFingerprintUpdates();
|
||||
subscribeToModerationEvents();
|
||||
updateInfoCallout();
|
||||
state.metadataStorageEnabled =
|
||||
typeof watchHistoryService.shouldStoreMetadata === "function"
|
||||
@@ -1893,6 +2682,7 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
if (elements.grid) {
|
||||
elements.grid.innerHTML = "";
|
||||
}
|
||||
clearHistoryCardRegistry();
|
||||
state.initialized = false;
|
||||
state.items = [];
|
||||
state.cursor = 0;
|
||||
@@ -1919,6 +2709,7 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
`[data-pointer-key="${escapeSelector(pointerKeyValue)}"]`
|
||||
);
|
||||
const parentDay = card?.closest("[data-history-day]");
|
||||
cleanupHistoryCard(pointerKeyValue);
|
||||
if (card) {
|
||||
card.remove();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
applyModerationContextDatasets,
|
||||
normalizeVideoModerationContext,
|
||||
} from "../moderationUiHelpers.js";
|
||||
import { buildModerationBadgeText } from "../moderationCopy.js";
|
||||
|
||||
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
||||
|
||||
@@ -981,130 +982,6 @@ export class VideoCard {
|
||||
return normalizeVideoModerationContext(this.video?.moderation);
|
||||
}
|
||||
|
||||
buildModerationReasonText(context) {
|
||||
if (!context) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const reasons = [];
|
||||
|
||||
if (context.trustedMuted) {
|
||||
const muteCount = Math.max(1, Number(context.trustedMuteCount) || 0);
|
||||
const muteLabel = muteCount === 1 ? "trusted contact" : "trusted contacts";
|
||||
reasons.push(`muted by ${muteCount === 1 ? "a" : muteCount} ${muteLabel}`);
|
||||
}
|
||||
|
||||
const typeLabel = context.friendlyType || "this video";
|
||||
const reportCount = Math.max(0, Number(context.trustedCount) || 0);
|
||||
if (reportCount > 0) {
|
||||
const friendLabel = reportCount === 1 ? "friend" : "friends";
|
||||
reasons.push(`${reportCount} ${friendLabel} reported ${typeLabel}`);
|
||||
} else if (!context.trustedMuted) {
|
||||
reasons.push(context.friendlyType ? `reports of ${typeLabel}` : "reports");
|
||||
}
|
||||
|
||||
if (!reasons.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const combined = reasons.join(" · ");
|
||||
return combined.charAt(0).toUpperCase() + combined.slice(1);
|
||||
}
|
||||
|
||||
buildHiddenSummaryLabel(context) {
|
||||
if (!context) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const reason = context.effectiveHideReason;
|
||||
if (!reason) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const countsSource =
|
||||
context.effectiveHideCounts || context.originalHideCounts || context.hideCounts || null;
|
||||
const getCount = (key, fallback) => {
|
||||
if (countsSource && Number.isFinite(countsSource[key])) {
|
||||
return Math.max(0, Number(countsSource[key]));
|
||||
}
|
||||
if (Number.isFinite(context[key])) {
|
||||
return Math.max(0, Number(context[key]));
|
||||
}
|
||||
return Math.max(0, Number(fallback) || 0);
|
||||
};
|
||||
|
||||
if (reason === "trusted-mute-hide") {
|
||||
const count = getCount("trustedMuteCount", context.trustedMuteCount || 0);
|
||||
if (count > 0) {
|
||||
const label = count === 1 ? "trusted mute" : "trusted mutes";
|
||||
return `${count} ${label}`;
|
||||
}
|
||||
return "trusted mute";
|
||||
}
|
||||
|
||||
if (reason === "trusted-report-hide") {
|
||||
const count = getCount("trustedReportCount", context.trustedCount || 0);
|
||||
const type = typeof context.friendlyType === "string" ? context.friendlyType.trim() : "";
|
||||
const normalizedType = type ? type.toLowerCase() : "";
|
||||
if (count > 0) {
|
||||
const label = count === 1 ? "report" : "reports";
|
||||
if (normalizedType) {
|
||||
return `${count} trusted ${normalizedType} ${label}`;
|
||||
}
|
||||
const base = count === 1 ? "trusted report" : "trusted reports";
|
||||
return `${count} ${base}`;
|
||||
}
|
||||
if (normalizedType) {
|
||||
return `trusted ${normalizedType} reports`;
|
||||
}
|
||||
return "trusted reports";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
buildModerationBadgeText(context) {
|
||||
if (!context) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const hiddenLabel = this.buildHiddenSummaryLabel(context);
|
||||
const reason = this.buildModerationReasonText(context);
|
||||
if (context.overrideActive) {
|
||||
if (hiddenLabel) {
|
||||
return `Showing despite ${hiddenLabel}`;
|
||||
}
|
||||
if (reason) {
|
||||
return `Showing despite ${reason}`;
|
||||
}
|
||||
return "Showing despite reports";
|
||||
}
|
||||
|
||||
if (context.activeHidden || (context.originalHidden && !context.overrideActive)) {
|
||||
if (hiddenLabel) {
|
||||
return `Hidden · ${hiddenLabel}`;
|
||||
}
|
||||
return "Hidden";
|
||||
}
|
||||
|
||||
const blurSource = (context.originalBlurReason || context.blurReason || "").toLowerCase();
|
||||
const suppressBlurLabel =
|
||||
blurSource === "trusted-mute" || blurSource === "trusted-mute-hide";
|
||||
|
||||
const parts = [];
|
||||
if (context.originalBlur && !suppressBlurLabel) {
|
||||
parts.push("Blurred");
|
||||
}
|
||||
if (context.originalBlockAutoplay) {
|
||||
parts.push("Autoplay blocked");
|
||||
}
|
||||
if (reason) {
|
||||
parts.push(reason);
|
||||
}
|
||||
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
createModerationOverrideButton() {
|
||||
const button = this.createElement("button", {
|
||||
classNames: ["moderation-badge__action", "flex-shrink-0"],
|
||||
@@ -1384,7 +1261,7 @@ export class VideoCard {
|
||||
|
||||
const text = this.createElement("span", {
|
||||
classNames: ["moderation-badge__text", "whitespace-nowrap"],
|
||||
textContent: this.buildModerationBadgeText(context),
|
||||
textContent: buildModerationBadgeText(context, { variant: "card" }),
|
||||
});
|
||||
badge.appendChild(text);
|
||||
|
||||
@@ -1555,7 +1432,7 @@ export class VideoCard {
|
||||
delete badge.dataset.moderationHideReason;
|
||||
}
|
||||
|
||||
const textContent = this.buildModerationBadgeText(context);
|
||||
const textContent = buildModerationBadgeText(context, { variant: "card" });
|
||||
if (this.moderationBadgeTextEl) {
|
||||
this.moderationBadgeTextEl.textContent = textContent;
|
||||
}
|
||||
@@ -1715,7 +1592,7 @@ export class VideoCard {
|
||||
|
||||
const summaryContainer = this.ensureHiddenSummaryContainer();
|
||||
if (summaryContainer) {
|
||||
const description = this.buildModerationBadgeText(context);
|
||||
const description = buildModerationBadgeText(context, { variant: "card" });
|
||||
if (description) {
|
||||
summaryContainer.setAttribute("aria-label", description);
|
||||
} else {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
formatViewCount,
|
||||
} from "../../viewCounter.js";
|
||||
import { normalizeVideoModerationContext } from "../moderationUiHelpers.js";
|
||||
import { buildModerationBadgeText } from "../moderationCopy.js";
|
||||
import {
|
||||
formatAbsoluteTimestamp,
|
||||
formatTimeAgo,
|
||||
@@ -3902,68 +3903,6 @@ export class VideoModal {
|
||||
return this.moderationBadgeId;
|
||||
}
|
||||
|
||||
buildModerationReasonText(context) {
|
||||
if (!context) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const reasons = [];
|
||||
|
||||
if (context.trustedMuted) {
|
||||
const muteCount = Math.max(1, Number(context.trustedMuteCount) || 0);
|
||||
const muteLabel = muteCount === 1 ? "trusted contact" : "trusted contacts";
|
||||
reasons.push(
|
||||
`muted by ${muteCount === 1 ? "a" : muteCount} ${muteLabel}`,
|
||||
);
|
||||
}
|
||||
|
||||
const typeLabel = context.friendlyType || "this video";
|
||||
const reportCount = Math.max(0, Number(context.trustedCount) || 0);
|
||||
if (reportCount > 0) {
|
||||
const friendLabel = reportCount === 1 ? "friend" : "friends";
|
||||
reasons.push(`${reportCount} ${friendLabel} reported ${typeLabel}`);
|
||||
} else if (!context.trustedMuted) {
|
||||
reasons.push(
|
||||
context.friendlyType ? `reports of ${typeLabel}` : "reports",
|
||||
);
|
||||
}
|
||||
|
||||
if (!reasons.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const combined = reasons.join(" · ");
|
||||
return combined.charAt(0).toUpperCase() + combined.slice(1);
|
||||
}
|
||||
|
||||
buildModerationBadgeText(context) {
|
||||
if (!context) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const blurSource = (context.originalBlurReason || context.blurReason || "")
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
const suppressBlurLabel =
|
||||
blurSource === "trusted-mute" || blurSource === "trusted-mute-hide";
|
||||
|
||||
const parts = [];
|
||||
if (!suppressBlurLabel) {
|
||||
parts.push("Blurred");
|
||||
}
|
||||
|
||||
const reason = this.buildModerationReasonText(context);
|
||||
if (reason) {
|
||||
parts.push(reason);
|
||||
}
|
||||
|
||||
if (!parts.length) {
|
||||
return "Blurred due to reports";
|
||||
}
|
||||
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
refreshActiveVideoModeration({ video = this.activeVideo } = {}) {
|
||||
if (!video) {
|
||||
this.activeModerationContext = null;
|
||||
@@ -4038,7 +3977,7 @@ export class VideoModal {
|
||||
badge.id = badgeId;
|
||||
}
|
||||
|
||||
const textContent = this.buildModerationBadgeText(context);
|
||||
const textContent = buildModerationBadgeText(context, { variant: "modal" });
|
||||
if (textEl) {
|
||||
textEl.textContent = textContent;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
// js/ui/moderationCopy.js
|
||||
|
||||
export function buildModerationReasonText(context) {
|
||||
if (!context) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const reasons = [];
|
||||
|
||||
if (context.trustedMuted) {
|
||||
const muteCount = Math.max(1, Number(context.trustedMuteCount) || 0);
|
||||
const muteLabel = muteCount === 1 ? "trusted contact" : "trusted contacts";
|
||||
const countLabel = muteCount === 1 ? "a" : muteCount;
|
||||
reasons.push(`muted by ${countLabel} ${muteLabel}`);
|
||||
}
|
||||
|
||||
const typeLabel = context.friendlyType || "this video";
|
||||
const reportCount = Math.max(0, Number(context.trustedCount) || 0);
|
||||
if (reportCount > 0) {
|
||||
const friendLabel = reportCount === 1 ? "friend" : "friends";
|
||||
reasons.push(`${reportCount} ${friendLabel} reported ${typeLabel}`);
|
||||
} else if (!context.trustedMuted) {
|
||||
reasons.push(context.friendlyType ? `reports of ${typeLabel}` : "reports");
|
||||
}
|
||||
|
||||
if (!reasons.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const combined = reasons.join(" · ");
|
||||
return combined.charAt(0).toUpperCase() + combined.slice(1);
|
||||
}
|
||||
|
||||
export function buildHiddenSummaryLabel(context) {
|
||||
if (!context) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const reason = context.effectiveHideReason;
|
||||
if (!reason) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const countsSource =
|
||||
context.effectiveHideCounts || context.originalHideCounts || context.hideCounts || null;
|
||||
|
||||
const getCount = (key, fallback) => {
|
||||
if (countsSource && Number.isFinite(countsSource[key])) {
|
||||
return Math.max(0, Number(countsSource[key]));
|
||||
}
|
||||
if (Number.isFinite(context[key])) {
|
||||
return Math.max(0, Number(context[key]));
|
||||
}
|
||||
return Math.max(0, Number(fallback) || 0);
|
||||
};
|
||||
|
||||
if (reason === "trusted-mute-hide") {
|
||||
const count = getCount("trustedMuteCount", context.trustedMuteCount || 0);
|
||||
if (count > 0) {
|
||||
const label = count === 1 ? "trusted mute" : "trusted mutes";
|
||||
return `${count} ${label}`;
|
||||
}
|
||||
return "trusted mute";
|
||||
}
|
||||
|
||||
if (reason === "trusted-report-hide") {
|
||||
const count = getCount("trustedReportCount", context.trustedCount || 0);
|
||||
const type = typeof context.friendlyType === "string" ? context.friendlyType.trim() : "";
|
||||
const normalizedType = type ? type.toLowerCase() : "";
|
||||
if (count > 0) {
|
||||
const label = count === 1 ? "report" : "reports";
|
||||
if (normalizedType) {
|
||||
return `${count} trusted ${normalizedType} ${label}`;
|
||||
}
|
||||
const base = count === 1 ? "trusted report" : "trusted reports";
|
||||
return `${count} ${base}`;
|
||||
}
|
||||
if (normalizedType) {
|
||||
return `trusted ${normalizedType} reports`;
|
||||
}
|
||||
return "trusted reports";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
export function buildModerationBadgeText(context, options = {}) {
|
||||
if (!context) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const variant = typeof options.variant === "string" ? options.variant : "card";
|
||||
|
||||
if (variant === "modal") {
|
||||
const blurSource = (context.originalBlurReason || context.blurReason || "")
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
const suppressBlurLabel =
|
||||
blurSource === "trusted-mute" || blurSource === "trusted-mute-hide";
|
||||
|
||||
const parts = [];
|
||||
if (!suppressBlurLabel) {
|
||||
parts.push("Blurred");
|
||||
}
|
||||
|
||||
const reason = buildModerationReasonText(context);
|
||||
if (reason) {
|
||||
parts.push(reason);
|
||||
}
|
||||
|
||||
if (!parts.length) {
|
||||
return "Blurred due to reports";
|
||||
}
|
||||
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
const hiddenLabel = buildHiddenSummaryLabel(context);
|
||||
const reason = buildModerationReasonText(context);
|
||||
|
||||
if (context.overrideActive) {
|
||||
if (hiddenLabel) {
|
||||
return `Showing despite ${hiddenLabel}`;
|
||||
}
|
||||
if (reason) {
|
||||
return `Showing despite ${reason}`;
|
||||
}
|
||||
return "Showing despite reports";
|
||||
}
|
||||
|
||||
if (context.activeHidden || (context.originalHidden && !context.overrideActive)) {
|
||||
if (hiddenLabel) {
|
||||
return `Hidden · ${hiddenLabel}`;
|
||||
}
|
||||
return "Hidden";
|
||||
}
|
||||
|
||||
const blurSource = (context.originalBlurReason || context.blurReason || "").toLowerCase();
|
||||
const suppressBlurLabel =
|
||||
blurSource === "trusted-mute" || blurSource === "trusted-mute-hide";
|
||||
|
||||
const parts = [];
|
||||
if (context.originalBlur && !suppressBlurLabel) {
|
||||
parts.push("Blurred");
|
||||
}
|
||||
if (context.originalBlockAutoplay) {
|
||||
parts.push("Autoplay blocked");
|
||||
}
|
||||
if (reason) {
|
||||
parts.push(reason);
|
||||
}
|
||||
|
||||
return parts.join(" · ");
|
||||
}
|
||||
@@ -1687,6 +1687,13 @@ async function testHistoryCardsUseDecryptedPlaybackMetadata() {
|
||||
"magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567",
|
||||
infoHash: "0123456789abcdef0123456789abcdef01234567",
|
||||
legacyInfoHash: "0123456789abcdef0123456789abcdef01234567",
|
||||
moderation: {
|
||||
blurThumbnail: true,
|
||||
original: { blurThumbnail: true },
|
||||
trustedCount: 3,
|
||||
reportType: "nudity",
|
||||
summary: { types: { nudity: { trusted: 3 } } }
|
||||
},
|
||||
};
|
||||
|
||||
const item = {
|
||||
@@ -1761,16 +1768,50 @@ async function testHistoryCardsUseDecryptedPlaybackMetadata() {
|
||||
this._classSet = new Set();
|
||||
this._className = "";
|
||||
this.classList = new FakeClassList(this);
|
||||
this._listeners = new Map();
|
||||
this.ownerDocument = null;
|
||||
}
|
||||
|
||||
appendChild(child) {
|
||||
if (child && typeof child === "object") {
|
||||
child.parentNode = this;
|
||||
if (this.ownerDocument) {
|
||||
child.ownerDocument = this.ownerDocument;
|
||||
}
|
||||
}
|
||||
this.children.push(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
insertBefore(child, reference) {
|
||||
if (child && typeof child === "object") {
|
||||
child.parentNode = this;
|
||||
if (this.ownerDocument) {
|
||||
child.ownerDocument = this.ownerDocument;
|
||||
}
|
||||
}
|
||||
if (!reference) {
|
||||
this.children.push(child);
|
||||
return child;
|
||||
}
|
||||
const index = this.children.indexOf(reference);
|
||||
if (index === -1) {
|
||||
this.children.push(child);
|
||||
} else {
|
||||
this.children.splice(index, 0, child);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
removeChild(child) {
|
||||
const index = this.children.indexOf(child);
|
||||
if (index !== -1) {
|
||||
this.children.splice(index, 1);
|
||||
child.parentNode = null;
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
get className() {
|
||||
return this._className;
|
||||
}
|
||||
@@ -1794,6 +1835,51 @@ async function testHistoryCardsUseDecryptedPlaybackMetadata() {
|
||||
removeAttribute(name) {
|
||||
delete this.attributes[name];
|
||||
}
|
||||
|
||||
getAttribute(name) {
|
||||
return Object.prototype.hasOwnProperty.call(this.attributes, name)
|
||||
? this.attributes[name]
|
||||
: null;
|
||||
}
|
||||
|
||||
addEventListener(type, handler) {
|
||||
if (typeof handler !== "function") {
|
||||
return;
|
||||
}
|
||||
if (!this._listeners.has(type)) {
|
||||
this._listeners.set(type, new Set());
|
||||
}
|
||||
this._listeners.get(type).add(handler);
|
||||
}
|
||||
|
||||
removeEventListener(type, handler) {
|
||||
const listeners = this._listeners.get(type);
|
||||
if (listeners) {
|
||||
listeners.delete(handler);
|
||||
}
|
||||
}
|
||||
|
||||
dispatchEvent(event) {
|
||||
if (!event || typeof event.type !== "string") {
|
||||
return false;
|
||||
}
|
||||
const listeners = this._listeners.get(event.type);
|
||||
if (!listeners) {
|
||||
return true;
|
||||
}
|
||||
for (const handler of Array.from(listeners)) {
|
||||
handler.call(this, event);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
closest() {
|
||||
return null;
|
||||
}
|
||||
|
||||
get parentElement() {
|
||||
return this.parentNode instanceof FakeElement ? this.parentNode : null;
|
||||
}
|
||||
}
|
||||
|
||||
function collectElements(root, predicate, results = []) {
|
||||
@@ -1811,13 +1897,38 @@ async function testHistoryCardsUseDecryptedPlaybackMetadata() {
|
||||
|
||||
const fakeDocument = {
|
||||
createElement(tagName) {
|
||||
return new FakeElement(tagName);
|
||||
const el = new FakeElement(tagName);
|
||||
el.ownerDocument = fakeDocument;
|
||||
return el;
|
||||
},
|
||||
createElementNS(_ns, tagName) {
|
||||
const el = new FakeElement(tagName);
|
||||
el.ownerDocument = fakeDocument;
|
||||
return el;
|
||||
}
|
||||
};
|
||||
|
||||
globalThis.document = fakeDocument;
|
||||
globalThis.HTMLElement = FakeElement;
|
||||
|
||||
const originalApp = getApplication();
|
||||
const overrideCalls = [];
|
||||
setApplication({
|
||||
handleModerationOverride({ video }) {
|
||||
overrideCalls.push(video);
|
||||
return true;
|
||||
},
|
||||
handleModerationHide() {
|
||||
return true;
|
||||
},
|
||||
decorateVideoModeration(videoInput) {
|
||||
return videoInput;
|
||||
},
|
||||
safeEncodeNpub(value) {
|
||||
return value;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const card = buildHistoryCard({
|
||||
item,
|
||||
@@ -1847,7 +1958,50 @@ async function testHistoryCardsUseDecryptedPlaybackMetadata() {
|
||||
pointerVideo.magnet,
|
||||
"play action should surface magnet from metadata",
|
||||
);
|
||||
|
||||
const blurredThumbs = collectElements(
|
||||
card,
|
||||
(element) => element.dataset?.thumbnailState === "blurred",
|
||||
);
|
||||
assert(
|
||||
blurredThumbs.length >= 1,
|
||||
"card should flag blurred thumbnails when moderation requests it",
|
||||
);
|
||||
|
||||
const blurredAvatars = collectElements(
|
||||
card,
|
||||
(element) => element.dataset?.visualState === "blurred",
|
||||
);
|
||||
assert(
|
||||
blurredAvatars.length >= 1,
|
||||
"card should blur creator avatars when moderation requests it",
|
||||
);
|
||||
|
||||
const overrideButtons = collectElements(
|
||||
card,
|
||||
(element) =>
|
||||
element.tagName === "BUTTON" &&
|
||||
element.dataset.moderationAction === "override",
|
||||
);
|
||||
assert.equal(
|
||||
overrideButtons.length,
|
||||
1,
|
||||
"card should render a moderation override button",
|
||||
);
|
||||
|
||||
overrideButtons[0].dispatchEvent({
|
||||
type: "click",
|
||||
preventDefault() {},
|
||||
stopPropagation() {},
|
||||
});
|
||||
await Promise.resolve();
|
||||
assert.equal(
|
||||
overrideCalls.length,
|
||||
1,
|
||||
"override button should call the app override handler",
|
||||
);
|
||||
} finally {
|
||||
setApplication(originalApp || null);
|
||||
globalThis.document = originalDocument;
|
||||
globalThis.HTMLElement = originalHTMLElement;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user