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:
thePR0M3TH3AN authored and GitHub committed 2025-10-25 20:22:49 -04:00
commit a4ac9dd104
6 files changed
+1126 -192

No files matched your search

+20 -1
View File
@@ -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 {
+791
View File
@@ -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();
}
+4 -127
View File
@@ -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 {
+2 -63
View File
@@ -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;
}
+154
View File
@@ -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(" · ");
}
+155 -1
View File
@@ -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;
}