mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-28 16:32:28 +00:00
Decorate channel videos with moderation helpers
This commit is contained in:
1 parent
1e19b2466e
commit
a0c6812e1b
3 files changed
+618
-69
No files matched your search
@@ -2028,7 +2028,27 @@ class Application {
|
||||
}
|
||||
|
||||
computeSimilarContentCandidates({ activeVideo = this.currentVideo, maxItems = 5 } = {}) {
|
||||
const target = activeVideo && typeof activeVideo === "object" ? activeVideo : null;
|
||||
const decorateCandidate = (video) => {
|
||||
if (!video || typeof video !== "object") {
|
||||
return video;
|
||||
}
|
||||
if (typeof this.decorateVideoModeration === "function") {
|
||||
try {
|
||||
const decorated = this.decorateVideoModeration(video);
|
||||
if (decorated && typeof decorated === "object") {
|
||||
return decorated;
|
||||
}
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[Application] Failed to decorate similar content candidate",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
return video;
|
||||
};
|
||||
|
||||
const target = activeVideo && typeof activeVideo === "object" ? decorateCandidate(activeVideo) : null;
|
||||
if (!target) {
|
||||
return [];
|
||||
}
|
||||
@@ -2095,35 +2115,36 @@ class Application {
|
||||
const results = [];
|
||||
|
||||
for (const candidate of candidateSource) {
|
||||
if (!candidate || typeof candidate !== "object") {
|
||||
const decoratedCandidate = decorateCandidate(candidate);
|
||||
if (!decoratedCandidate || typeof decoratedCandidate !== "object") {
|
||||
continue;
|
||||
}
|
||||
if (candidate === target) {
|
||||
if (decoratedCandidate === target) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidateId = typeof candidate.id === "string" ? candidate.id : "";
|
||||
const candidateId = typeof decoratedCandidate.id === "string" ? decoratedCandidate.id : "";
|
||||
if (candidateId && candidateId === activeId) {
|
||||
continue;
|
||||
}
|
||||
if (candidate.deleted === true) {
|
||||
if (decoratedCandidate.deleted === true) {
|
||||
continue;
|
||||
}
|
||||
if (candidate.isPrivate === true) {
|
||||
if (decoratedCandidate.isPrivate === true) {
|
||||
continue;
|
||||
}
|
||||
if (candidate.isNsfw === true && ALLOW_NSFW_CONTENT !== true) {
|
||||
if (decoratedCandidate.isNsfw === true && ALLOW_NSFW_CONTENT !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidatePubkey = typeof candidate.pubkey === "string" ? candidate.pubkey : "";
|
||||
const candidatePubkey = typeof decoratedCandidate.pubkey === "string" ? decoratedCandidate.pubkey : "";
|
||||
if (candidatePubkey && this.isAuthorBlocked(candidatePubkey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidateTagsSource = Array.isArray(candidate.displayTags) && candidate.displayTags.length
|
||||
? candidate.displayTags
|
||||
: collectVideoTags(candidate);
|
||||
const candidateTagsSource = Array.isArray(decoratedCandidate.displayTags) && decoratedCandidate.displayTags.length
|
||||
? decoratedCandidate.displayTags
|
||||
: collectVideoTags(decoratedCandidate);
|
||||
if (!Array.isArray(candidateTagsSource) || candidateTagsSource.length === 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -2176,26 +2197,26 @@ class Application {
|
||||
: [];
|
||||
}
|
||||
|
||||
let postedAt = this.getKnownVideoPostedAt(candidate);
|
||||
if (!Number.isFinite(postedAt) && Number.isFinite(candidate.rootCreatedAt)) {
|
||||
postedAt = Math.floor(candidate.rootCreatedAt);
|
||||
let postedAt = this.getKnownVideoPostedAt(decoratedCandidate);
|
||||
if (!Number.isFinite(postedAt) && Number.isFinite(decoratedCandidate.rootCreatedAt)) {
|
||||
postedAt = Math.floor(decoratedCandidate.rootCreatedAt);
|
||||
}
|
||||
if (!Number.isFinite(postedAt) && Number.isFinite(candidate.created_at)) {
|
||||
postedAt = Math.floor(candidate.created_at);
|
||||
if (!Number.isFinite(postedAt) && Number.isFinite(decoratedCandidate.created_at)) {
|
||||
postedAt = Math.floor(decoratedCandidate.created_at);
|
||||
}
|
||||
if (!Number.isFinite(postedAt)) {
|
||||
postedAt = null;
|
||||
}
|
||||
|
||||
let shareUrl = "";
|
||||
if (typeof candidate.shareUrl === "string" && candidate.shareUrl.trim()) {
|
||||
shareUrl = candidate.shareUrl.trim();
|
||||
if (typeof decoratedCandidate.shareUrl === "string" && decoratedCandidate.shareUrl.trim()) {
|
||||
shareUrl = decoratedCandidate.shareUrl.trim();
|
||||
} else if (candidateId) {
|
||||
shareUrl = this.buildShareUrlFromEventId(candidateId) || "";
|
||||
}
|
||||
|
||||
results.push({
|
||||
video: candidate,
|
||||
video: decoratedCandidate,
|
||||
pointerInfo: pointerInfo || null,
|
||||
shareUrl,
|
||||
postedAt,
|
||||
|
||||
+353
-50
@@ -16,6 +16,10 @@ import { createChannelProfileMenuPanel } from "./ui/components/videoMenuRenderer
|
||||
import { ALLOW_NSFW_CONTENT } from "./config.js";
|
||||
import { sanitizeProfileMediaUrl } from "./utils/profileMedia.js";
|
||||
import moderationService from "./services/moderationService.js";
|
||||
import {
|
||||
applyModerationContextDatasets,
|
||||
normalizeVideoModerationContext,
|
||||
} from "./ui/moderationUiHelpers.js";
|
||||
import {
|
||||
calculateZapShares,
|
||||
describeShareType,
|
||||
@@ -64,72 +68,336 @@ function normalizeHex(value) {
|
||||
return /^[0-9a-f]{64}$/.test(trimmed) ? trimmed : "";
|
||||
}
|
||||
|
||||
function shouldBlurChannelMedia(pubkey) {
|
||||
function decorateChannelVideo(video, app = getApp()) {
|
||||
if (!video || typeof video !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof app?.decorateVideoModeration === "function") {
|
||||
try {
|
||||
const decorated = app.decorateVideoModeration(video);
|
||||
if (decorated && typeof decorated === "object") {
|
||||
return decorated;
|
||||
}
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[ChannelProfile] Failed to decorate channel video moderation",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return video;
|
||||
}
|
||||
|
||||
function collectChannelVideos(pubkey, app = getApp()) {
|
||||
if (!app || !(app.videosMap instanceof Map)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const normalized = normalizeHex(pubkey);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
return [];
|
||||
}
|
||||
|
||||
const matches = [];
|
||||
app.videosMap.forEach((video) => {
|
||||
if (!video || typeof video !== "object") {
|
||||
return;
|
||||
}
|
||||
const videoPubkey = normalizeHex(video.pubkey);
|
||||
if (videoPubkey === normalized) {
|
||||
matches.push(video);
|
||||
}
|
||||
});
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
function buildChannelModerationFallback(pubkey) {
|
||||
const normalized = normalizeHex(pubkey);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let viewerMuted = false;
|
||||
try {
|
||||
if (moderationService.isAuthorMutedByViewer(normalized) === true) {
|
||||
return true;
|
||||
viewerMuted = moderationService.isAuthorMutedByViewer(normalized) === true;
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[ChannelProfile] Failed to resolve viewer mute state for channel",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
let trustedMuted = false;
|
||||
let trustedMuters = [];
|
||||
try {
|
||||
if (typeof moderationService.isAuthorMutedByTrusted === "function") {
|
||||
trustedMuted =
|
||||
moderationService.isAuthorMutedByTrusted(normalized) === true;
|
||||
}
|
||||
if (trustedMuted) {
|
||||
if (typeof moderationService.getTrustedMutersForAuthor === "function") {
|
||||
const muters =
|
||||
moderationService.getTrustedMutersForAuthor(normalized) || [];
|
||||
if (Array.isArray(muters)) {
|
||||
trustedMuters = muters;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
devLogger.warn("[ChannelProfile] Failed to resolve viewer mute state", error);
|
||||
devLogger.warn(
|
||||
"[ChannelProfile] Failed to resolve trusted mute state for channel",
|
||||
error,
|
||||
);
|
||||
trustedMuted = false;
|
||||
trustedMuters = [];
|
||||
}
|
||||
|
||||
const app = getApp();
|
||||
const videos =
|
||||
app?.videosMap instanceof Map ? Array.from(app.videosMap.values()) : [];
|
||||
if (!viewerMuted && !trustedMuted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const video of videos) {
|
||||
if (!video || typeof video !== "object") {
|
||||
continue;
|
||||
}
|
||||
const videoPubkey =
|
||||
typeof video.pubkey === "string" ? video.pubkey.trim().toLowerCase() : "";
|
||||
if (videoPubkey !== normalized) {
|
||||
const trustedMuteCount = Array.isArray(trustedMuters)
|
||||
? trustedMuters.length
|
||||
: 0;
|
||||
|
||||
const fallback = {
|
||||
trustedMuted,
|
||||
trustedMuters: Array.isArray(trustedMuters) ? trustedMuters.slice() : [],
|
||||
trustedMuteCount,
|
||||
};
|
||||
|
||||
if (viewerMuted) {
|
||||
fallback.blurThumbnail = true;
|
||||
fallback.blurReason = "viewer-mute";
|
||||
fallback.hidden = true;
|
||||
fallback.hideReason = "viewer-mute";
|
||||
fallback.hideCounts = {
|
||||
trustedMuteCount,
|
||||
trustedReportCount: 0,
|
||||
};
|
||||
} else if (trustedMuted) {
|
||||
fallback.blurThumbnail = true;
|
||||
fallback.blurReason = "trusted-mute";
|
||||
}
|
||||
|
||||
fallback.original = {
|
||||
blockAutoplay: fallback.blockAutoplay === true,
|
||||
blurThumbnail: fallback.blurThumbnail === true,
|
||||
hidden: fallback.hidden === true,
|
||||
hideReason: fallback.hideReason || "",
|
||||
hideCounts: fallback.hideCounts ? { ...fallback.hideCounts } : undefined,
|
||||
hideBypass: fallback.hideBypass || "",
|
||||
hideTriggered:
|
||||
fallback.hidden === true || Boolean(fallback.hideReason) || false,
|
||||
blurReason: fallback.blurReason || "",
|
||||
};
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function resolveChannelModeration(pubkey, app = getApp()) {
|
||||
const normalized = normalizeHex(pubkey);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const matches = collectChannelVideos(normalized, app);
|
||||
let fallbackModeration = null;
|
||||
|
||||
for (const candidate of matches) {
|
||||
const decorated = decorateChannelVideo(candidate, app);
|
||||
if (!decorated || typeof decorated !== "object") {
|
||||
continue;
|
||||
}
|
||||
const moderation =
|
||||
video.moderation && typeof video.moderation === "object"
|
||||
? video.moderation
|
||||
decorated.moderation && typeof decorated.moderation === "object"
|
||||
? decorated.moderation
|
||||
: null;
|
||||
if (!moderation) {
|
||||
continue;
|
||||
}
|
||||
if (moderation.hidden === true) {
|
||||
return true;
|
||||
|
||||
if (moderation.viewerOverride?.showAnyway === true) {
|
||||
return moderation;
|
||||
}
|
||||
if (moderation.blurThumbnail === true) {
|
||||
const reason =
|
||||
typeof moderation.blurReason === "string"
|
||||
? moderation.blurReason.trim().toLowerCase()
|
||||
: "";
|
||||
if (reason.startsWith("trusted-")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (moderation.hidden === true || moderation.blurThumbnail === true) {
|
||||
return moderation;
|
||||
}
|
||||
|
||||
if (!fallbackModeration) {
|
||||
fallbackModeration = moderation;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
if (fallbackModeration) {
|
||||
return fallbackModeration;
|
||||
}
|
||||
|
||||
return buildChannelModerationFallback(normalized);
|
||||
}
|
||||
|
||||
function applyChannelVisualBlur({ bannerEl = null, avatarEl = null, pubkey = "" } = {}) {
|
||||
const shouldBlur = shouldBlurChannelMedia(pubkey);
|
||||
const applyState = (element) => {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
if (shouldBlur) {
|
||||
element.dataset.visualState = "blurred";
|
||||
} else if (element.dataset.visualState) {
|
||||
delete element.dataset.visualState;
|
||||
}
|
||||
};
|
||||
export function applyChannelVisualBlur({
|
||||
bannerEl = null,
|
||||
avatarEl = null,
|
||||
pubkey = "",
|
||||
app = null,
|
||||
} = {}) {
|
||||
const resolvedApp = app || getApp();
|
||||
const moderation = resolveChannelModeration(pubkey, resolvedApp) || {};
|
||||
const context = normalizeVideoModerationContext(moderation);
|
||||
|
||||
applyState(bannerEl);
|
||||
applyState(avatarEl);
|
||||
const resolvedBanner = bannerEl ||
|
||||
(typeof document !== "undefined"
|
||||
? document.getElementById("channelBanner")
|
||||
: null);
|
||||
const resolvedAvatar = avatarEl ||
|
||||
(typeof document !== "undefined"
|
||||
? document.getElementById("channelAvatar")
|
||||
: null);
|
||||
|
||||
const avatarElements = [resolvedAvatar, resolvedBanner].filter(Boolean);
|
||||
|
||||
applyModerationContextDatasets(context, {
|
||||
root: resolvedBanner || null,
|
||||
avatar: resolvedAvatar || null,
|
||||
avatars: avatarElements.length > 1 ? avatarElements : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function updateChannelModerationVisuals({
|
||||
pubkey = "",
|
||||
app = null,
|
||||
bannerEl = null,
|
||||
avatarEl = null,
|
||||
} = {}) {
|
||||
if (typeof document === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedApp = app || getApp();
|
||||
const targetPubkey = pubkey || currentChannelHex || "";
|
||||
if (!targetPubkey) {
|
||||
applyChannelVisualBlur({
|
||||
bannerEl,
|
||||
avatarEl,
|
||||
pubkey: "",
|
||||
app: resolvedApp,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedBanner =
|
||||
bannerEl || document.getElementById("channelBanner");
|
||||
const resolvedAvatar =
|
||||
avatarEl || document.getElementById("channelAvatar");
|
||||
|
||||
applyChannelVisualBlur({
|
||||
bannerEl: resolvedBanner,
|
||||
avatarEl: resolvedAvatar,
|
||||
pubkey: targetPubkey,
|
||||
app: resolvedApp,
|
||||
});
|
||||
}
|
||||
|
||||
let channelModerationEventsBound = false;
|
||||
const channelModerationUnsubscribes = [];
|
||||
|
||||
function handleTrustedMuteSignal() {
|
||||
if (!currentChannelHex) {
|
||||
return;
|
||||
}
|
||||
updateChannelModerationVisuals({ pubkey: currentChannelHex });
|
||||
}
|
||||
|
||||
function handleModerationSummarySignal(detail = {}) {
|
||||
if (!detail || typeof detail !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventId = typeof detail.eventId === "string" ? detail.eventId : "";
|
||||
if (!eventId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const app = getApp();
|
||||
const video = app?.videosMap instanceof Map ? app.videosMap.get(eventId) : null;
|
||||
if (!video || typeof video !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
const videoPubkey = normalizeHex(video.pubkey);
|
||||
if (!videoPubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channelPubkey = normalizeHex(currentChannelHex);
|
||||
if (!channelPubkey || videoPubkey !== channelPubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
decorateChannelVideo(video, app);
|
||||
updateChannelModerationVisuals({
|
||||
pubkey: channelPubkey,
|
||||
app,
|
||||
});
|
||||
}
|
||||
|
||||
function handleVideoModerationEvent(event) {
|
||||
const detail = event?.detail;
|
||||
const video = detail && typeof detail === "object" ? detail.video : null;
|
||||
if (!video || typeof video !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
const videoPubkey = normalizeHex(video.pubkey);
|
||||
const channelPubkey = normalizeHex(currentChannelHex);
|
||||
if (!videoPubkey || !channelPubkey || videoPubkey !== channelPubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const app = getApp();
|
||||
decorateChannelVideo(video, app);
|
||||
if (app?.videosMap instanceof Map && typeof video.id === "string") {
|
||||
app.videosMap.set(video.id, video);
|
||||
}
|
||||
updateChannelModerationVisuals({
|
||||
pubkey: channelPubkey,
|
||||
app,
|
||||
});
|
||||
}
|
||||
|
||||
function ensureChannelModerationEvents() {
|
||||
if (channelModerationEventsBound) {
|
||||
return;
|
||||
}
|
||||
channelModerationEventsBound = true;
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
document.addEventListener("video:moderation-override", handleVideoModerationEvent);
|
||||
document.addEventListener("video:moderation-hide", handleVideoModerationEvent);
|
||||
}
|
||||
|
||||
if (moderationService && typeof moderationService.on === "function") {
|
||||
const trustedUnsub = moderationService.on(
|
||||
"trusted-mutes",
|
||||
handleTrustedMuteSignal,
|
||||
);
|
||||
if (typeof trustedUnsub === "function") {
|
||||
channelModerationUnsubscribes.push(trustedUnsub);
|
||||
}
|
||||
|
||||
const summaryUnsub = moderationService.on(
|
||||
"summary",
|
||||
handleModerationSummarySignal,
|
||||
);
|
||||
if (typeof summaryUnsub === "function") {
|
||||
channelModerationUnsubscribes.push(summaryUnsub);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const summarizeZapTracker = (tracker) =>
|
||||
@@ -2461,6 +2729,10 @@ export async function initChannelProfileView() {
|
||||
|
||||
const app = getApp();
|
||||
|
||||
ensureChannelModerationEvents();
|
||||
|
||||
updateChannelModerationVisuals({ app, pubkey: currentChannelHex });
|
||||
|
||||
setupChannelShareButton();
|
||||
setupChannelMoreMenu();
|
||||
|
||||
@@ -2512,6 +2784,28 @@ export async function initChannelProfileView() {
|
||||
await Promise.allSettled(pendingTasks);
|
||||
}
|
||||
|
||||
export function __setChannelProfileTestState({ pubkey, npub } = {}) {
|
||||
if (typeof pubkey !== "undefined") {
|
||||
if (typeof pubkey === "string" && pubkey) {
|
||||
currentChannelHex = pubkey;
|
||||
} else {
|
||||
currentChannelHex = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof npub !== "undefined") {
|
||||
if (typeof npub === "string" && npub) {
|
||||
currentChannelNpub = npub;
|
||||
} else {
|
||||
currentChannelNpub = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function __ensureChannelModerationEventsForTests() {
|
||||
ensureChannelModerationEvents();
|
||||
}
|
||||
|
||||
function setupZapButton({ force = false } = {}) {
|
||||
const zapButton = getChannelZapButton();
|
||||
if (!zapButton) {
|
||||
@@ -3123,7 +3417,7 @@ function buildRenderableChannelVideos({ events = [], app } = {}) {
|
||||
return videos;
|
||||
}
|
||||
|
||||
function renderChannelVideosFromList({
|
||||
export function renderChannelVideosFromList({
|
||||
videos = [],
|
||||
container,
|
||||
app,
|
||||
@@ -3254,11 +3548,14 @@ function renderChannelVideosFromList({
|
||||
|
||||
const allowNsfw = ALLOW_NSFW_CONTENT === true;
|
||||
|
||||
videos.forEach((video) => {
|
||||
if (!video || !video.id || !video.title) {
|
||||
return;
|
||||
for (const originalVideo of videos) {
|
||||
if (!originalVideo || !originalVideo.id || !originalVideo.title) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const decoratedVideo = decorateChannelVideo(originalVideo, app) || originalVideo;
|
||||
const video = decoratedVideo;
|
||||
|
||||
const normalizedVideoPubkey = normalizePubkey(video.pubkey);
|
||||
const canEdit =
|
||||
Boolean(normalizedVideoPubkey) &&
|
||||
@@ -3266,11 +3563,11 @@ function renderChannelVideosFromList({
|
||||
normalizedVideoPubkey === normalizedViewerPubkey;
|
||||
|
||||
if (video.isPrivate && !canEdit) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!allowNsfw && video?.isNsfw === true && !canEdit) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
app?.videosMap?.set(video.id, video);
|
||||
@@ -3447,7 +3744,7 @@ function renderChannelVideosFromList({
|
||||
if (cardEl) {
|
||||
fragment.appendChild(cardEl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (renderIndex === 0) {
|
||||
if (allowEmptyMessage) {
|
||||
@@ -3473,6 +3770,8 @@ function renderChannelVideosFromList({
|
||||
|
||||
app?.attachMoreMenuHandlers?.(container);
|
||||
|
||||
updateChannelModerationVisuals({ app });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3561,7 +3860,11 @@ function applyChannelProfileMetadata({
|
||||
}
|
||||
|
||||
const blurPubkey = pubkey || currentChannelHex || "";
|
||||
applyChannelVisualBlur({ bannerEl, avatarEl, pubkey: blurPubkey });
|
||||
updateChannelModerationVisuals({
|
||||
bannerEl,
|
||||
avatarEl,
|
||||
pubkey: blurPubkey,
|
||||
});
|
||||
|
||||
const nameEl = document.getElementById("channelName");
|
||||
if (nameEl) {
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
import {
|
||||
renderChannelVideosFromList,
|
||||
applyChannelVisualBlur,
|
||||
__setChannelProfileTestState,
|
||||
__ensureChannelModerationEventsForTests,
|
||||
} from "../../js/channelProfile.js";
|
||||
import { nostrClient } from "../../js/nostrClientFacade.js";
|
||||
import moderationService from "../../js/services/moderationService.js";
|
||||
import { withMockedNostrTools, createModerationAppHarness } from "../helpers/moderation-test-helpers.mjs";
|
||||
|
||||
function setupDom(t) {
|
||||
const dom = new JSDOM(
|
||||
"<!DOCTYPE html><html><body>" +
|
||||
'<div id="channelBanner"></div>' +
|
||||
'<img id="channelAvatar" />' +
|
||||
'<div id="channelVideoList"></div>' +
|
||||
"</body></html>",
|
||||
{ url: "https://example.invalid/channel" },
|
||||
);
|
||||
|
||||
const { window } = dom;
|
||||
const { document } = window;
|
||||
|
||||
const previousGlobals = {
|
||||
window: globalThis.window,
|
||||
document: globalThis.document,
|
||||
HTMLElement: globalThis.HTMLElement,
|
||||
CustomEvent: globalThis.CustomEvent,
|
||||
Event: globalThis.Event,
|
||||
Node: globalThis.Node,
|
||||
navigator: globalThis.navigator,
|
||||
location: globalThis.location,
|
||||
MutationObserver: globalThis.MutationObserver,
|
||||
IntersectionObserver: globalThis.IntersectionObserver,
|
||||
};
|
||||
|
||||
globalThis.window = window;
|
||||
globalThis.document = document;
|
||||
globalThis.HTMLElement = window.HTMLElement;
|
||||
globalThis.CustomEvent = window.CustomEvent;
|
||||
globalThis.Event = window.Event;
|
||||
globalThis.Node = window.Node;
|
||||
globalThis.navigator = window.navigator;
|
||||
globalThis.location = window.location;
|
||||
globalThis.MutationObserver = window.MutationObserver;
|
||||
globalThis.IntersectionObserver =
|
||||
window.IntersectionObserver ||
|
||||
class {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
takeRecords() {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
t.after(() => {
|
||||
Object.entries(previousGlobals).forEach(([key, value]) => {
|
||||
if (typeof value === "undefined") {
|
||||
delete globalThis[key];
|
||||
} else {
|
||||
globalThis[key] = value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return { document, window };
|
||||
}
|
||||
|
||||
function createVideoFixture(overrides = {}) {
|
||||
return {
|
||||
id: overrides.id || "v".repeat(64),
|
||||
title: overrides.title || "Fixture Video",
|
||||
pubkey: overrides.pubkey || "a".repeat(64),
|
||||
created_at: overrides.created_at || Math.floor(Date.now() / 1000),
|
||||
url: overrides.url || "https://example.invalid/video.mp4",
|
||||
moderation: overrides.moderation || {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("renderChannelVideosFromList decorates videos before storing", async (t) => {
|
||||
const { document } = setupDom(t);
|
||||
withMockedNostrTools(t);
|
||||
|
||||
nostrClient.allEvents = new Map();
|
||||
nostrClient.rawEvents = new Set();
|
||||
|
||||
const app = await createModerationAppHarness();
|
||||
app.loadedThumbnails = new Map();
|
||||
app.videosMap = new Map();
|
||||
app.buildShareUrlFromEventId = (id) => `https://example.invalid/watch/${id}`;
|
||||
app.formatTimeAgo = () => "just now";
|
||||
app.ensureGlobalMoreMenuHandlers = () => {};
|
||||
app.closeAllMoreMenus = () => {};
|
||||
app.normalizeHexPubkey = (value) =>
|
||||
typeof value === "string" ? value.trim().toLowerCase() : "";
|
||||
app.hasOlderVersion = () => false;
|
||||
app.deriveVideoPointerInfo = () => null;
|
||||
app.persistWatchHistoryMetadataForVideo = () => {};
|
||||
app.canCurrentUserManageBlacklist = () => false;
|
||||
app.isMagnetUriSupported = () => false;
|
||||
app.mountVideoListView = () => {};
|
||||
app.mediaLoader = { observe() {} };
|
||||
app.attachMoreMenuHandlers = () => {};
|
||||
|
||||
const originalDecorate = app.decorateVideoModeration;
|
||||
let decoratedCount = 0;
|
||||
app.decorateVideoModeration = function decorated(video) {
|
||||
decoratedCount += 1;
|
||||
if (!video.moderation || typeof video.moderation !== "object") {
|
||||
video.moderation = {};
|
||||
}
|
||||
video.moderation.decoratedByTest = true;
|
||||
return originalDecorate.call(this, video);
|
||||
};
|
||||
|
||||
const container = document.getElementById("channelVideoList");
|
||||
const video = createVideoFixture();
|
||||
|
||||
const rendered = renderChannelVideosFromList({
|
||||
videos: [video],
|
||||
container,
|
||||
app,
|
||||
loadToken: 0,
|
||||
});
|
||||
|
||||
assert.equal(rendered, true);
|
||||
assert.equal(decoratedCount, 1);
|
||||
const stored = app.videosMap.get(video.id);
|
||||
assert.ok(stored, "decorated video stored in videosMap");
|
||||
assert.equal(stored.moderation.decoratedByTest, true);
|
||||
});
|
||||
|
||||
test("applyChannelVisualBlur blurs banner and avatar when viewer mutes author", async (t) => {
|
||||
const { document } = setupDom(t);
|
||||
withMockedNostrTools(t);
|
||||
|
||||
const bannerEl = document.getElementById("channelBanner");
|
||||
const avatarEl = document.getElementById("channelAvatar");
|
||||
const pubkey = "b".repeat(64);
|
||||
|
||||
const originalMute = moderationService.isAuthorMutedByViewer;
|
||||
moderationService.isAuthorMutedByViewer = () => true;
|
||||
t.after(() => {
|
||||
moderationService.isAuthorMutedByViewer = originalMute;
|
||||
});
|
||||
|
||||
applyChannelVisualBlur({
|
||||
bannerEl,
|
||||
avatarEl,
|
||||
pubkey,
|
||||
app: { videosMap: new Map() },
|
||||
});
|
||||
|
||||
assert.equal(bannerEl.dataset.visualState, "blurred");
|
||||
assert.equal(avatarEl.dataset.visualState, "blurred");
|
||||
});
|
||||
|
||||
test("moderation override clears channel blur via event wiring", async (t) => {
|
||||
const { document } = setupDom(t);
|
||||
withMockedNostrTools(t);
|
||||
|
||||
nostrClient.allEvents = new Map();
|
||||
nostrClient.rawEvents = new Set();
|
||||
|
||||
const app = await createModerationAppHarness();
|
||||
app.loadedThumbnails = new Map();
|
||||
app.videosMap = new Map();
|
||||
app.buildShareUrlFromEventId = (id) => `https://example.invalid/watch/${id}`;
|
||||
app.formatTimeAgo = () => "just now";
|
||||
app.ensureGlobalMoreMenuHandlers = () => {};
|
||||
app.closeAllMoreMenus = () => {};
|
||||
app.normalizeHexPubkey = (value) =>
|
||||
typeof value === "string" ? value.trim().toLowerCase() : "";
|
||||
app.hasOlderVersion = () => false;
|
||||
app.deriveVideoPointerInfo = () => null;
|
||||
app.persistWatchHistoryMetadataForVideo = () => {};
|
||||
app.canCurrentUserManageBlacklist = () => false;
|
||||
app.isMagnetUriSupported = () => false;
|
||||
app.mountVideoListView = () => {};
|
||||
app.mediaLoader = { observe() {} };
|
||||
app.attachMoreMenuHandlers = () => {};
|
||||
app.getActiveModerationThresholds = () => ({
|
||||
blurThreshold: 1,
|
||||
autoplayBlockThreshold: 1,
|
||||
trustedMuteHideThreshold: 1,
|
||||
trustedSpamHideThreshold: Number.POSITIVE_INFINITY,
|
||||
});
|
||||
|
||||
const channelPubkey = "c".repeat(64);
|
||||
__setChannelProfileTestState({ pubkey: channelPubkey });
|
||||
__ensureChannelModerationEventsForTests();
|
||||
|
||||
const container = document.getElementById("channelVideoList");
|
||||
const moderationState = {
|
||||
trustedCount: 2,
|
||||
summary: {
|
||||
types: { nudity: { trusted: 2 } },
|
||||
},
|
||||
};
|
||||
const video = createVideoFixture({ pubkey: channelPubkey, moderation: moderationState });
|
||||
|
||||
renderChannelVideosFromList({
|
||||
videos: [video],
|
||||
container,
|
||||
app,
|
||||
loadToken: 0,
|
||||
});
|
||||
|
||||
const bannerEl = document.getElementById("channelBanner");
|
||||
const avatarEl = document.getElementById("channelAvatar");
|
||||
assert.equal(bannerEl.dataset.visualState, "blurred");
|
||||
assert.equal(avatarEl.dataset.visualState, "blurred");
|
||||
|
||||
const cardStub = { refreshModerationUi() {} };
|
||||
app.handleModerationOverride({ video, card: cardStub });
|
||||
|
||||
assert.equal(bannerEl.dataset.visualState, undefined);
|
||||
assert.equal(avatarEl.dataset.visualState, undefined);
|
||||
});
|
||||
Reference in new issue
Block a user