mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 17:02:28 +00:00
Track channel video cards to refresh moderation state
This commit is contained in:
1 parent
fb33f79a65
commit
b53e5afc22
3 files changed
+186
-7
No files matched your search
@@ -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
@@ -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");
|
||||
});
|
||||
Reference in new issue
Block a user