diff --git a/js/subscriptions.js b/js/subscriptions.js index dd10460e..f089d389 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -1140,6 +1140,46 @@ class SubscriptionsManager { listView.setPopularTagsContainer(null); } + const buildModerationPayload = (detail = {}) => { + const event = detail?.event || null; + const trigger = + detail?.trigger || + (event && (event.currentTarget || event.target)) || + null; + const card = detail?.card || null; + const video = detail?.video || null; + const datasetContext = (() => { + const detailContext = + typeof detail?.context === "string" ? detail.context.trim() : ""; + if (detailContext) { + return detailContext; + } + const datasetSource = + (detail?.dataset && typeof detail.dataset === "object" + ? detail.dataset + : null) || + (trigger && trigger.dataset) || + (card && card.root && card.root.dataset) || + null; + if ( + datasetSource && + typeof datasetSource.context === "string" && + datasetSource.context.trim() + ) { + return datasetSource.context.trim(); + } + return ""; + })(); + + return { + ...detail, + video, + card, + trigger, + context: datasetContext || "subscriptions", + }; + }; + listView.setPlaybackHandler((detail) => { if (!detail) { return; @@ -1207,6 +1247,20 @@ class SubscriptionsManager { app?.handleMoreMenuAction?.("blacklist-author", detail); }); + listView.setModerationOverrideHandler((detail = {}) => { + if (typeof app?.handleModerationOverride !== "function") { + return false; + } + return app.handleModerationOverride(buildModerationPayload(detail)); + }); + + listView.setModerationHideHandler((detail = {}) => { + if (typeof app?.handleModerationHide !== "function") { + return false; + } + return app.handleModerationHide(buildModerationPayload(detail)); + }); + listView.addEventListener("video:share", (event) => { const detail = event?.detail || {}; const dataset = { diff --git a/tests/subscriptions-manager.test.mjs b/tests/subscriptions-manager.test.mjs index bfe1d218..52868a7f 100644 --- a/tests/subscriptions-manager.test.mjs +++ b/tests/subscriptions-manager.test.mjs @@ -677,3 +677,137 @@ test("renderSameGridStyle shows empty state message", async () => { } }); +test( + "renderSameGridStyle forwards moderation badge actions to the application", + async () => { + const SubscriptionsManager = subscriptions.constructor; + + const originalWindow = globalThis.window; + const originalDocument = globalThis.document; + const originalNavigator = globalThis.navigator; + + const dom = new JSDOM( + "
", + { url: "https://example.test/" }, + ); + + globalThis.window = dom.window; + globalThis.document = dom.window.document; + globalThis.navigator = dom.window.navigator; + + const manager = new SubscriptionsManager(); + + const overrideCalls = []; + const hideCalls = []; + + const app = { + videosMap: new Map(), + handleModerationOverride(detail) { + overrideCalls.push(detail); + return true; + }, + handleModerationHide(detail) { + hideCalls.push(detail); + return true; + }, + ensureGlobalMoreMenuHandlers() {}, + closeAllMoreMenus() {}, + handleMoreMenuAction() {}, + }; + + setApplication(app); + + const video = { + id: "video-moderated-1", + pubkey: "author-1", + title: "Moderated clip", + created_at: 1, + moderation: { + original: { hidden: true }, + trustedMuted: true, + trustedMuteCount: 2, + summary: { types: { nudity: { trusted: 1 } } }, + }, + }; + + manager.renderSameGridStyle( + { items: [{ video }] }, + "subscriptionsVideoList", + ); + + const container = dom.window.document.getElementById( + "subscriptionsVideoList", + ); + const overrideButton = container.querySelector( + '[data-moderation-action="override"]', + ); + assert.ok(overrideButton, "override button should render for moderated video"); + + overrideButton.click(); + await Promise.resolve(); + + assert.equal( + overrideCalls.length, + 1, + "app.handleModerationOverride should receive one call", + ); + const overrideDetail = overrideCalls[0]; + assert.equal(overrideDetail.video, video); + assert.equal(overrideDetail.card?.video, video); + assert.equal( + overrideDetail.context, + "subscriptions", + "override payload should include the subscriptions context", + ); + assert.equal(overrideDetail.trigger, overrideButton); + + const hideButton = container.querySelector('[data-moderation-action="hide"]'); + assert.ok(hideButton, "hide button should render for moderated video"); + + hideButton.click(); + await Promise.resolve(); + + assert.equal( + hideCalls.length, + 1, + "app.handleModerationHide should receive one call", + ); + const hideDetail = hideCalls[0]; + assert.equal(hideDetail.video, video); + assert.equal(hideDetail.card?.video, video); + assert.equal( + hideDetail.context, + "subscriptions", + "hide payload should include the subscriptions context", + ); + assert.equal(hideDetail.trigger, hideButton); + + if (manager.subscriptionListView?.destroy) { + manager.subscriptionListView.destroy(); + manager.subscriptionListView = null; + } + + dom.window.close(); + + if (typeof originalWindow === "undefined") { + delete globalThis.window; + } else { + globalThis.window = originalWindow; + } + + if (typeof originalDocument === "undefined") { + delete globalThis.document; + } else { + globalThis.document = originalDocument; + } + + if (typeof originalNavigator === "undefined") { + delete globalThis.navigator; + } else { + globalThis.navigator = originalNavigator; + } + + setApplication(null); + }, +); +