diff --git a/js/ui/applicationBootstrap.js b/js/ui/applicationBootstrap.js index 4d5fd701..ebd96633 100644 --- a/js/ui/applicationBootstrap.js +++ b/js/ui/applicationBootstrap.js @@ -1122,6 +1122,24 @@ export default class ApplicationBootstrap { ), ); } + // #24: "Always show this creator" overlay button → set the account-level + // web-of-trust override for that author + refresh the feed. Fired by the + // moderation badge on video cards / the player / similar cards. + if (typeof document !== "undefined") { + const onTrustAuthor = (event) => { + const pubkey = + typeof event?.detail?.pubkey === "string" ? event.detail.pubkey : ""; + if (!pubkey) { + return; + } + setAuthorModerationOverride(pubkey, { showAnyway: true }); + notifyAuthorModerationOverrideChanged(app, pubkey); + }; + document.addEventListener("video:trust-author", onTrustAuthor); + nostrUnsubscribes.push(() => + document.removeEventListener("video:trust-author", onTrustAuthor), + ); + } const submissionsPollId = setInterval(() => { void app.refreshSubmissionsIndicator({ reason: "poll" }); }, 180000); diff --git a/js/ui/components/SimilarContentCard.js b/js/ui/components/SimilarContentCard.js index 93705b69..8f35a33f 100644 --- a/js/ui/components/SimilarContentCard.js +++ b/js/ui/components/SimilarContentCard.js @@ -134,6 +134,8 @@ export class SimilarContentCard { this.handleModerationHideClick(event); this.boundModerationBlockHandler = (event) => this.handleModerationBlockClick(event); + this.boundTrustAuthorHandler = (event) => + this.handleTrustAuthorClick(event); this.build(); } @@ -524,6 +526,43 @@ export class SimilarContentCard { return button; } + createModerationTrustAuthorButton() { + const button = this.document.createElement("button"); + button.classList.add( + "moderation-badge__action", + "flex-shrink-0", + "text-xs", + "py-1", + "px-2" + ); + button.type = "button"; + button.dataset.moderationAction = "trust-author"; + button.setAttribute("aria-label", "Always show videos from this creator"); + button.textContent = "Always show creator"; + button.addEventListener("click", this.boundTrustAuthorHandler); + button.classList.add("pointer-events-auto"); + return button; + } + + handleTrustAuthorClick(event) { + event?.preventDefault?.(); + event?.stopPropagation?.(); + + const pubkey = + this.video?.pubkey || this.video?.author?.pubkey || ""; + if (!pubkey || typeof document === "undefined") { + return; + } + + try { + document.dispatchEvent( + new CustomEvent("video:trust-author", { detail: { pubkey } }) + ); + } catch (error) { + // Non-fatal: trust-author is a convenience action. + } + } + handleShowAnywayClick(event) { event?.preventDefault?.(); event?.stopPropagation?.(); @@ -720,6 +759,7 @@ export class SimilarContentCard { actions.appendChild(showButton); this.moderationActionButton = showButton; this.moderationActionButtonMode = "override"; + actions.appendChild(this.createModerationTrustAuthorButton()); } } diff --git a/js/ui/components/VideoCard.js b/js/ui/components/VideoCard.js index 39016da4..da66cdaa 100644 --- a/js/ui/components/VideoCard.js +++ b/js/ui/components/VideoCard.js @@ -156,6 +156,8 @@ export class VideoCard { this.moderationBadgeSlot = null; this.hiddenSummaryEl = null; this.boundShowAnywayHandler = (event) => this.handleShowAnywayClick(event); + this.boundTrustAuthorHandler = (event) => + this.handleTrustAuthorClick(event); this.boundModerationBlockHandler = (event) => this.handleModerationBlockClick(event); this.boundModerationHideHandler = (event) => @@ -1210,6 +1212,44 @@ export class VideoCard { return button; } + // Account-level ("trusted creator") action shown alongside "Show anyway": + // turns off the web-of-trust warning for EVERY video by this creator. Fires a + // document event that the app handles (sets the per-author override + refresh) + // — no per-renderer callback plumbing needed. + createModerationTrustAuthorButton() { + const button = this.createElement("button", { + classNames: ["moderation-badge__action", "flex-shrink-0"], + attrs: { + type: "button", + "data-moderation-action": "trust-author", + "aria-describedby": this.getModerationBadgeId(), + "aria-label": "Always show videos from this creator", + }, + textContent: "Always show creator", + }); + button.addEventListener("click", this.boundTrustAuthorHandler); + return button; + } + + handleTrustAuthorClick(event) { + if (event) { + event.preventDefault?.(); + event.stopPropagation?.(); + } + const pubkey = + this.video?.pubkey || this.video?.author?.pubkey || ""; + if (!pubkey || typeof document === "undefined") { + return; + } + try { + document.dispatchEvent( + new CustomEvent("video:trust-author", { detail: { pubkey } }), + ); + } catch (error) { + userLogger.warn("[VideoCard] trust-author dispatch failed", error); + } + } + createModerationHideButton() { const { text: label, ariaLabel } = getModerationOverrideActionLabels({ overrideActive: true, @@ -1638,6 +1678,7 @@ export class VideoCard { actions.appendChild(showButton); this.moderationActionButton = showButton; this.moderationActionButtonMode = "override"; + actions.appendChild(this.createModerationTrustAuthorButton()); } hasActions = true; } else { diff --git a/js/ui/components/VideoModal.js b/js/ui/components/VideoModal.js index e2a2d772..3d6789d5 100644 --- a/js/ui/components/VideoModal.js +++ b/js/ui/components/VideoModal.js @@ -251,6 +251,7 @@ export class VideoModal { this.moderationBadgeText = null; this.moderationActionsContainer = null; this.moderationPrimaryButton = null; + this.moderationTrustAuthorButton = null; this.moderationBlockButton = null; this.moderationPrimaryMode = ""; this.moderationBadgeId = ""; @@ -268,6 +269,8 @@ export class VideoModal { this.handleModerationHideClick.bind(this); this.handleModerationBlockClick = this.handleModerationBlockClick.bind(this); + this.handleModerationTrustAuthorClick = + this.handleModerationTrustAuthorClick.bind(this); this.handleGlobalModerationOverride = this.handleGlobalModerationOverride.bind(this); this.handleGlobalModerationBlock = @@ -556,6 +559,12 @@ export class VideoModal { ); } } + if (this.moderationTrustAuthorButton) { + this.moderationTrustAuthorButton.removeEventListener( + "click", + this.handleModerationTrustAuthorClick, + ); + } if (this.moderationBlockButton) { this.moderationBlockButton.removeEventListener( "click", @@ -568,6 +577,7 @@ export class VideoModal { this.moderationActionsContainer = null; this.moderationPrimaryButton = null; this.moderationPrimaryMode = ""; + this.moderationTrustAuthorButton = null; this.moderationBlockButton = null; const previousScrollRegion = this.scrollRegion; @@ -2009,8 +2019,28 @@ export class VideoModal { } actionsAttached = true; } + + if (mode === "override") { + const trustButton = this.ensureModerationTrustAuthorButton(); + if (trustButton) { + trustButton.disabled = false; + trustButton.removeAttribute("aria-busy"); + if (badgeId) { + trustButton.setAttribute("aria-describedby", badgeId); + } else { + trustButton.removeAttribute("aria-describedby"); + } + if (actions && trustButton.parentElement !== actions) { + actions.appendChild(trustButton); + } + actionsAttached = true; + } + } else { + this.removeModerationTrustAuthorButton(); + } } else { this.removeModerationPrimaryButton(); + this.removeModerationTrustAuthorButton(); } if (this.shouldShowModerationBlockAction(context)) { @@ -2160,6 +2190,65 @@ export class VideoModal { this.moderationPrimaryMode = ""; } + ensureModerationTrustAuthorButton() { + if (!this.document) { + return null; + } + + if (!this.moderationTrustAuthorButton) { + const button = this.document.createElement("button"); + button.type = "button"; + button.className = "moderation-badge__action flex-shrink-0"; + button.dataset.moderationAction = "trust-author"; + button.textContent = "Always show creator"; + button.setAttribute( + "aria-label", + "Always show videos from this creator" + ); + button.addEventListener("click", this.handleModerationTrustAuthorClick); + this.moderationTrustAuthorButton = button; + } + + return this.moderationTrustAuthorButton; + } + + removeModerationTrustAuthorButton() { + const button = this.moderationTrustAuthorButton; + if (!button) { + return; + } + + button.removeEventListener("click", this.handleModerationTrustAuthorClick); + if (button.parentElement) { + button.parentElement.removeChild(button); + } + + this.moderationTrustAuthorButton = null; + } + + handleModerationTrustAuthorClick(event) { + if (event) { + event.preventDefault?.(); + event.stopPropagation?.(); + } + + const pubkey = + this.activeVideo?.pubkey || + this.activeVideo?.author?.pubkey || + ""; + if (!pubkey || typeof document === "undefined") { + return; + } + + try { + document.dispatchEvent( + new CustomEvent("video:trust-author", { detail: { pubkey } }) + ); + } catch (error) { + devLogger?.warn?.("[VideoModal] trust-author dispatch failed", error); + } + } + ensureModerationBlockButton() { if (!this.document) { return null;