Track channel video cards to refresh moderation state

This commit is contained in:
thePR0M3TH3AN committed 2025-10-25 21:37:10 -04:00
1 parent fb33f79a65
commit b53e5afc22
3 files changed
+186 -7

No files matched your search

+5 -1
View File
@@ -61,7 +61,10 @@ import WatchHistoryController from "./ui/watchHistoryController.js";
import WatchHistoryTelemetry from "./services/watchHistoryTelemetry.js";
import { getSidebarLoadingMarkup } from "./sidebarLoading.js";
import { subscriptions } from "./subscriptions.js";
import { refreshActiveChannelVideoGrid } from "./channelProfile.js";
import {
refreshActiveChannelVideoGrid,
clearChannelVideoCardRegistry,
} from "./channelProfile.js";
import { isWatchHistoryDebugEnabled } from "./watchHistoryDebug.js";
import { devLogger, userLogger } from "./utils/logger.js";
import createPopover from "./ui/overlay/popoverEngine.js";
@@ -1486,6 +1489,7 @@ class Application {
}
prepareForViewLoad() {
clearChannelVideoCardRegistry();
if (this.videoListView) {
this.videoListView.destroy();
}
+97 -6
View File
@@ -82,6 +82,12 @@ const channelModerationBadgeState = {
app: null,
};
const channelVideoCardsById = new Map();
export function clearChannelVideoCardRegistry() {
channelVideoCardsById.clear();
}
let currentChannelHex = null;
let currentChannelNpub = null;
let currentChannelLightningAddress = "";
@@ -828,6 +834,64 @@ function handleTrustedMuteSignal() {
updateChannelModerationVisuals({ pubkey: currentChannelHex });
}
function refreshChannelVideoCardModeration(
videoId,
{ app = getApp(), fallbackVideo = null, decoratedVideo = null } = {},
) {
if (typeof videoId !== "string" || !videoId) {
return false;
}
const resolvedApp = app || getApp();
const sourceMap =
resolvedApp?.videosMap instanceof Map ? resolvedApp.videosMap : null;
const card = channelVideoCardsById.get(videoId) || null;
let nextVideo =
decoratedVideo && typeof decoratedVideo === "object"
? decoratedVideo
: null;
if (!nextVideo) {
const baseVideo =
(sourceMap && sourceMap.get(videoId)) ||
(fallbackVideo && typeof fallbackVideo === "object"
? fallbackVideo
: null) ||
(card && typeof card.video === "object" ? card.video : null);
if (!baseVideo) {
return false;
}
nextVideo = decorateChannelVideo(baseVideo, resolvedApp) || baseVideo;
}
if (sourceMap && nextVideo) {
sourceMap.set(videoId, nextVideo);
}
if (!card || !nextVideo) {
return Boolean(nextVideo);
}
card.video = nextVideo;
if (typeof card.refreshModerationUi === "function") {
try {
card.refreshModerationUi();
} catch (error) {
devLogger.warn(
`[ChannelProfile] Failed to refresh moderation UI for video ${videoId}`,
error,
);
}
}
channelVideoCardsById.set(videoId, card);
return true;
}
function handleModerationSummarySignal(detail = {}) {
if (!detail || typeof detail !== "object") {
return;
@@ -854,7 +918,12 @@ function handleModerationSummarySignal(detail = {}) {
return;
}
decorateChannelVideo(video, app);
const decoratedVideo = decorateChannelVideo(video, app) || video;
refreshChannelVideoCardModeration(eventId, {
app,
fallbackVideo: decoratedVideo,
decoratedVideo,
});
updateChannelModerationVisuals({
pubkey: channelPubkey,
app,
@@ -874,11 +943,22 @@ function handleVideoModerationEvent(event) {
return;
}
const app = getApp();
decorateChannelVideo(video, app);
if (app?.videosMap instanceof Map && typeof video.id === "string") {
app.videosMap.set(video.id, video);
const videoId = typeof video.id === "string" ? video.id : "";
if (!videoId) {
return;
}
const app = getApp();
const decoratedVideo = decorateChannelVideo(video, app) || video;
if (app?.videosMap instanceof Map) {
app.videosMap.set(videoId, decoratedVideo);
}
refreshChannelVideoCardModeration(videoId, {
app,
fallbackVideo: decoratedVideo,
decoratedVideo,
});
updateChannelModerationVisuals({
pubkey: channelPubkey,
app,
@@ -3939,10 +4019,19 @@ export async function renderChannelVideosFromList({
loadToken,
allowEmptyMessage = true
} = {}) {
if (!container || loadToken !== currentVideoLoadToken) {
if (!container) {
if (loadToken === currentVideoLoadToken) {
clearChannelVideoCardRegistry();
}
return false;
}
if (loadToken !== currentVideoLoadToken) {
return false;
}
clearChannelVideoCardRegistry();
if (!Array.isArray(videos) || videos.length === 0) {
if (allowEmptyMessage) {
container.dataset.hasChannelVideos = "false";
@@ -4194,6 +4283,8 @@ export async function renderChannelVideosFromList({
}
});
channelVideoCardsById.set(video.id, videoCard);
videoCard.onPlay = ({ event: domEvent, video: cardVideo }) => {
const trigger = domEvent?.currentTarget || domEvent?.target;
const detail = extractPlaybackDetail(trigger, cardVideo || video);
@@ -431,3 +431,87 @@ test("channel header moderation badge reflects blur state and override actions",
assert.equal(overrideContext.overrideActive, true);
assert.ok(overrideText.startsWith("Showing despite"));
});
test("channel video cards update moderation state in place when summary changes", 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: Number.POSITIVE_INFINITY,
trustedMuteHideThreshold: Number.POSITIVE_INFINITY,
trustedSpamHideThreshold: Number.POSITIVE_INFINITY,
});
setApplication(app);
t.after(() => setApplication(null));
const video = createVideoFixture();
__setChannelProfileTestState({ pubkey: video.pubkey });
__ensureChannelModerationEventsForTests();
const container = document.getElementById("channelVideoList");
await renderChannelVideosFromList({
videos: [video],
container,
app,
loadToken: 0,
});
const cardEl = container.querySelector('[data-component="video-card"]');
assert.ok(cardEl, "video card rendered for channel");
const thumbnailEl = cardEl.querySelector('[data-video-thumbnail="true"]');
assert.ok(thumbnailEl, "card thumbnail present");
assert.equal(
thumbnailEl.dataset.thumbnailState || "",
"",
"thumbnail should not be blurred initially",
);
const existing = app.videosMap.get(video.id) || video;
const updatedVideo = {
...existing,
moderation: {
...(existing?.moderation || {}),
summary: {
types: {
nudity: { trusted: 3 },
},
},
trustedCount: 3,
blurThumbnail: true,
reportType: "nudity",
},
};
app.videosMap.set(video.id, updatedVideo);
const priorCard = container.querySelector('[data-component="video-card"]');
moderationService.emit("summary", { eventId: video.id });
const currentCard = container.querySelector('[data-component="video-card"]');
assert.strictEqual(currentCard, priorCard, "card should not rerender");
assert.equal(thumbnailEl.dataset.thumbnailState, "blurred");
assert.equal(cardEl.dataset.moderationReportType, "nudity");
assert.equal(cardEl.dataset.moderationReportCount, "3");
});