Merge pull request #972 from PR0M3TH3AN/codex/create-moderation-action-controller

Refactor moderation actions into controller
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-27 14:13:24 -04:00
commit 7775ead0b2
2 files changed
+650 -271

No files matched your search

+93 -271
View File
@@ -142,6 +142,7 @@ import {
} from "./state/cache.js";
import ApplicationBootstrap from "./ui/applicationBootstrap.js";
import VideoModalCommentController from "./ui/videoModalCommentController.js";
import ModerationActionController from "./services/moderationActionController.js";
const recordVideoViewApi = (...args) => recordVideoView(nostrClient, ...args);
@@ -234,6 +235,8 @@ class Application {
}
}
);
this.initializeModerationActionController();
}
get modalVideo() {
@@ -5881,306 +5884,113 @@ class Application {
return video;
}
handleModerationOverride({ video, card }) {
if (!video || typeof video !== "object" || !video.id) {
return false;
initializeModerationActionController() {
if (this.moderationActionController) {
return this.moderationActionController;
}
try {
setModerationOverride(video.id, {
showAnyway: true,
updatedAt: Date.now(),
});
} catch (error) {
devLogger.warn("[Application] Failed to persist moderation override:", error);
}
this.moderationActionController = new ModerationActionController({
services: {
setModerationOverride,
clearModerationOverride,
userBlocks,
},
selectors: {
getVideoById: (id) =>
this.videosMap instanceof Map && id ? this.videosMap.get(id) : null,
getCurrentVideo: () => this.currentVideo,
},
actions: {
decorateVideoModeration: (video) => this.decorateVideoModeration(video),
resumePlayback: (video) => this.resumePendingModeratedPlayback(video),
refreshVideos: (payload) => this.onVideosShouldRefresh(payload),
showStatus: (message, options) => this.showStatus(message, options),
showError: (message) => this.showError(message),
describeBlockError: (error) => this.describeUserBlockActionError(error),
},
auth: {
isLoggedIn: () => this.isUserLoggedIn(),
getViewerPubkey: () => this.pubkey,
normalizePubkey: (value) => this.normalizeHexPubkey(value),
},
ui: {
refreshCardModerationUi: (card, options) =>
this.refreshCardModerationUi(card, options),
dispatchModerationEvent: (eventName, detail) =>
this.dispatchModerationEvent(eventName, detail),
},
});
const storedVideo =
this.videosMap instanceof Map && video.id ? this.videosMap.get(video.id) : null;
const target = storedVideo || video;
if (target) {
if (target.moderation && typeof target.moderation === "object") {
if (target.moderation.hidden) {
delete target.moderation.hidden;
}
if (target.moderation.hideReason) {
delete target.moderation.hideReason;
}
if (target.moderation.hideCounts) {
delete target.moderation.hideCounts;
}
if (target.moderation.hideBypass) {
delete target.moderation.hideBypass;
}
}
this.decorateVideoModeration(target);
}
if (this.currentVideo && this.currentVideo.id === video.id) {
if (this.currentVideo.moderation && typeof this.currentVideo.moderation === "object") {
if (this.currentVideo.moderation.hidden) {
delete this.currentVideo.moderation.hidden;
}
if (this.currentVideo.moderation.hideReason) {
delete this.currentVideo.moderation.hideReason;
}
if (this.currentVideo.moderation.hideCounts) {
delete this.currentVideo.moderation.hideCounts;
}
if (this.currentVideo.moderation.hideBypass) {
delete this.currentVideo.moderation.hideBypass;
}
}
this.decorateVideoModeration(this.currentVideo);
}
if (card && typeof card.refreshModerationUi === "function") {
try {
card.refreshModerationUi();
} catch (error) {
devLogger.warn("[Application] Failed to refresh moderation UI:", error);
}
}
const doc =
(this.videoModal && this.videoModal.document) ||
(typeof document !== "undefined" ? document : null);
if (doc && typeof doc.dispatchEvent === "function") {
try {
doc.dispatchEvent(
new CustomEvent("video:moderation-override", {
detail: { video: target },
}),
);
} catch (eventError) {
devLogger.warn(
"[Application] Failed to dispatch moderation override event:",
eventError,
);
}
}
this.resumePendingModeratedPlayback(target);
return true;
return this.moderationActionController;
}
async handleModerationBlock({ video, card }) {
if (!video || typeof video !== "object" || !video.id) {
return false;
}
if (!this.isUserLoggedIn()) {
this.showStatus("Log in to block accounts.", { showSpinner: false });
return false;
}
const viewerHex = this.normalizeHexPubkey(this.pubkey);
if (!viewerHex) {
this.showError("Select a profile before blocking accounts.");
return false;
}
const targetHex = this.normalizeHexPubkey(video?.pubkey);
if (!targetHex) {
this.showError("Unable to determine which account to block.");
return false;
}
if (viewerHex === targetHex) {
this.showError("You cannot block yourself.");
refreshCardModerationUi(card, { reason } = {}) {
if (!card || typeof card.refreshModerationUi !== "function") {
return false;
}
try {
await userBlocks.ensureLoaded(viewerHex);
card.refreshModerationUi();
return true;
} catch (error) {
const suffix = reason ? ` ${reason}` : "";
devLogger.warn(
"[Application] Failed to load block list before blocking:",
`[Application] Failed to refresh moderation UI${suffix}:`,
error,
);
this.showError("Unable to load your block list. Please try again.");
return false;
}
let alreadyBlocked =
typeof userBlocks.isBlocked === "function" &&
userBlocks.isBlocked(targetHex);
let blockApplied = false;
if (!alreadyBlocked) {
try {
const result = await userBlocks.addBlock(targetHex, viewerHex);
alreadyBlocked = true;
blockApplied = result?.already !== true;
} catch (error) {
const message =
(typeof this.describeUserBlockActionError === "function"
? this.describeUserBlockActionError(error)
: "") || "Failed to block this creator. Please try again.";
this.showError(message);
devLogger.warn("[Application] Failed to block creator:", error);
return false;
}
}
if (alreadyBlocked) {
const statusMessage = blockApplied
? "Creator blocked. Their videos will disappear from your feed."
: "Creator already blocked. Their videos will disappear from your feed.";
this.showStatus(statusMessage, { showSpinner: false });
}
try {
clearModerationOverride(video.id);
} catch (error) {
devLogger.warn("[Application] Failed to clear moderation override:", error);
}
const storedVideo =
this.videosMap instanceof Map && video.id ? this.videosMap.get(video.id) : null;
const target = storedVideo || video;
const resetModerationState = (subject) => {
if (!subject || typeof subject !== "object") {
return;
}
const moderation =
subject.moderation && typeof subject.moderation === "object"
? subject.moderation
: null;
if (!moderation) {
return;
}
if (moderation.viewerOverride) {
delete moderation.viewerOverride;
}
if (moderation.hideBypass) {
delete moderation.hideBypass;
}
};
if (target) {
resetModerationState(target);
this.decorateVideoModeration(target);
}
if (this.currentVideo && this.currentVideo.id === video.id) {
resetModerationState(this.currentVideo);
this.decorateVideoModeration(this.currentVideo);
}
if (card && typeof card.refreshModerationUi === "function") {
try {
card.refreshModerationUi();
} catch (error) {
devLogger.warn(
"[Application] Failed to refresh moderation UI after block:",
error,
);
}
}
const doc =
(this.videoModal && this.videoModal.document) ||
(typeof document !== "undefined" ? document : null);
if (doc && typeof doc.dispatchEvent === "function") {
try {
const detail = { video: target };
doc.dispatchEvent(new CustomEvent("video:moderation-block", { detail }));
doc.dispatchEvent(new CustomEvent("video:moderation-hide", { detail }));
} catch (eventError) {
devLogger.warn(
"[Application] Failed to dispatch moderation block event:",
eventError,
);
}
}
try {
await this.onVideosShouldRefresh({ reason: "user-block-update" });
} catch (error) {
devLogger.warn(
"[Application] Failed to refresh videos after block:",
error,
);
}
return true;
}
handleModerationHide({ video, card }) {
if (!video || typeof video !== "object" || !video.id) {
dispatchModerationEvent(eventName, detail = {}) {
const doc =
(this.videoModal && this.videoModal.document) ||
(typeof document !== "undefined" ? document : null);
if (!doc || typeof doc.dispatchEvent !== "function") {
return false;
}
try {
clearModerationOverride(video.id);
doc.dispatchEvent(new CustomEvent(eventName, { detail }));
return true;
} catch (error) {
devLogger.warn("[Application] Failed to clear moderation override:", error);
const eventLabels = {
"video:moderation-override": "moderation override event",
"video:moderation-block": "moderation block event",
"video:moderation-hide": "moderation hide event",
};
const label = eventLabels[eventName] || eventName;
devLogger.warn(`[Application] Failed to dispatch ${label}:`, error);
return false;
}
}
handleModerationOverride(payload = {}) {
const controller = this.initializeModerationActionController();
if (!controller) {
return false;
}
const storedVideo =
this.videosMap instanceof Map && video.id
? this.videosMap.get(video.id)
: null;
const target = storedVideo || video;
return controller.handleOverride(payload);
}
const resetOverrideState = (subject) => {
if (!subject || typeof subject !== "object") {
return;
}
const moderation =
subject.moderation && typeof subject.moderation === "object"
? subject.moderation
: null;
if (!moderation) {
return;
}
if (moderation.viewerOverride) {
delete moderation.viewerOverride;
}
};
if (target) {
resetOverrideState(target);
this.decorateVideoModeration(target);
async handleModerationBlock(payload = {}) {
const controller = this.initializeModerationActionController();
if (!controller) {
return false;
}
if (this.currentVideo && this.currentVideo.id === video.id) {
resetOverrideState(this.currentVideo);
this.decorateVideoModeration(this.currentVideo);
return controller.handleBlock(payload);
}
handleModerationHide(payload = {}) {
const controller = this.initializeModerationActionController();
if (!controller) {
return false;
}
if (card && typeof card.refreshModerationUi === "function") {
try {
card.refreshModerationUi();
} catch (error) {
devLogger.warn(
"[Application] Failed to refresh moderation UI after hide:",
error,
);
}
}
const doc =
(this.videoModal && this.videoModal.document) ||
(typeof document !== "undefined" ? document : null);
if (doc && typeof doc.dispatchEvent === "function") {
try {
doc.dispatchEvent(
new CustomEvent("video:moderation-hide", { detail: { video: target } }),
);
} catch (eventError) {
devLogger.warn(
"[Application] Failed to dispatch moderation hide event:",
eventError,
);
}
}
return true;
return controller.handleHide(payload);
}
getVideoAddressPointer(video) {
@@ -9188,6 +8998,18 @@ class Application {
this.teardownModalViewCountSubscription();
this.videoModalReadyPromise = null;
if (this.moderationActionController) {
try {
this.moderationActionController.destroy();
} catch (error) {
devLogger.warn(
"[Application] Failed to destroy moderation action controller:",
error,
);
}
this.moderationActionController = null;
}
if (this.statusAutoHideHandle) {
const ownerDocument =
this.statusContainer?.ownerDocument ||
+557
View File
@@ -0,0 +1,557 @@
import { devLogger } from "../utils/logger.js";
export default class ModerationActionController {
constructor({
services = {},
selectors = {},
actions = {},
auth = {},
ui = {},
} = {}) {
this.services = {
setModerationOverride:
typeof services.setModerationOverride === "function"
? services.setModerationOverride
: null,
clearModerationOverride:
typeof services.clearModerationOverride === "function"
? services.clearModerationOverride
: null,
userBlocks:
services.userBlocks && typeof services.userBlocks === "object"
? services.userBlocks
: null,
};
this.selectors = {
getVideoById:
typeof selectors.getVideoById === "function"
? selectors.getVideoById
: null,
getCurrentVideo:
typeof selectors.getCurrentVideo === "function"
? selectors.getCurrentVideo
: null,
};
this.actions = {
decorateVideoModeration:
typeof actions.decorateVideoModeration === "function"
? actions.decorateVideoModeration
: null,
resumePlayback:
typeof actions.resumePlayback === "function"
? actions.resumePlayback
: null,
refreshVideos:
typeof actions.refreshVideos === "function"
? actions.refreshVideos
: null,
showStatus:
typeof actions.showStatus === "function" ? actions.showStatus : null,
showError:
typeof actions.showError === "function" ? actions.showError : null,
describeBlockError:
typeof actions.describeBlockError === "function"
? actions.describeBlockError
: null,
};
this.auth = {
isLoggedIn:
typeof auth.isLoggedIn === "function" ? auth.isLoggedIn : null,
getViewerPubkey:
typeof auth.getViewerPubkey === "function"
? auth.getViewerPubkey
: null,
normalizePubkey:
typeof auth.normalizePubkey === "function"
? auth.normalizePubkey
: null,
};
this.ui = {
refreshCardModerationUi:
typeof ui.refreshCardModerationUi === "function"
? ui.refreshCardModerationUi
: null,
dispatchModerationEvent:
typeof ui.dispatchModerationEvent === "function"
? ui.dispatchModerationEvent
: null,
};
this.destroyed = false;
}
destroy() {
this.destroyed = true;
}
handleOverride({ video, card } = {}) {
if (this.destroyed) {
return false;
}
if (!video || typeof video !== "object" || !video.id) {
return false;
}
this.persistModerationOverride(video.id);
const target = this.resolveTargetVideo(video);
this.clearHideState(target);
this.decorateVideo(target);
const current = this.getCurrentVideo();
if (current && current.id === video.id) {
this.clearHideState(current);
this.decorateVideo(current);
}
this.refreshCard(card, { reason: "" });
this.dispatchModerationEvent("video:moderation-override", { video: target });
if (target) {
this.resumePlayback(target);
}
return true;
}
async handleBlock({ video, card } = {}) {
if (this.destroyed) {
return false;
}
if (!video || typeof video !== "object" || !video.id) {
return false;
}
const { userBlocks } = this.services;
if (!userBlocks) {
devLogger.warn(
"[ModerationActionController] Block action requested but userBlocks service is unavailable.",
);
return false;
}
if (!this.isUserLoggedIn()) {
this.showStatus("Log in to block accounts.", { showSpinner: false });
return false;
}
const viewerHex = this.getViewerHex();
if (!viewerHex) {
this.showError("Select a profile before blocking accounts.");
return false;
}
const targetHex = this.getTargetHex(video);
if (!targetHex) {
this.showError("Unable to determine which account to block.");
return false;
}
if (viewerHex === targetHex) {
this.showError("You cannot block yourself.");
return false;
}
try {
await userBlocks.ensureLoaded(viewerHex);
} catch (error) {
devLogger.warn(
"[ModerationActionController] Failed to load block list before blocking:",
error,
);
this.showError("Unable to load your block list. Please try again.");
return false;
}
let alreadyBlocked =
typeof userBlocks.isBlocked === "function" &&
userBlocks.isBlocked(targetHex);
let blockApplied = false;
if (!alreadyBlocked) {
try {
const result = await userBlocks.addBlock(targetHex, viewerHex);
alreadyBlocked = true;
blockApplied = result?.already !== true;
} catch (error) {
const message =
this.describeBlockError(error) ||
"Failed to block this creator. Please try again.";
this.showError(message);
devLogger.warn(
"[ModerationActionController] Failed to block creator:",
error,
);
return false;
}
}
if (alreadyBlocked) {
const statusMessage = blockApplied
? "Creator blocked. Their videos will disappear from your feed."
: "Creator already blocked. Their videos will disappear from your feed.";
this.showStatus(statusMessage, { showSpinner: false });
}
this.clearModerationOverride(video.id);
const target = this.resolveTargetVideo(video);
this.clearViewerOverride(target);
this.clearHideBypass(target);
this.decorateVideo(target);
const current = this.getCurrentVideo();
if (current && current.id === video.id) {
this.clearViewerOverride(current);
this.clearHideBypass(current);
this.decorateVideo(current);
}
this.refreshCard(card, { reason: "after block" });
const detail = { video: target };
this.dispatchModerationEvent("video:moderation-block", detail);
this.dispatchModerationEvent("video:moderation-hide", detail);
await this.refreshVideosAfterBlock();
return true;
}
handleHide({ video, card } = {}) {
if (this.destroyed) {
return false;
}
if (!video || typeof video !== "object" || !video.id) {
return false;
}
this.clearModerationOverride(video.id);
const target = this.resolveTargetVideo(video);
this.clearViewerOverride(target);
this.decorateVideo(target);
const current = this.getCurrentVideo();
if (current && current.id === video.id) {
this.clearViewerOverride(current);
this.decorateVideo(current);
}
this.refreshCard(card, { reason: "after hide" });
this.dispatchModerationEvent("video:moderation-hide", { video: target });
return true;
}
resolveTargetVideo(video) {
if (!video || typeof video !== "object" || !video.id) {
return null;
}
if (!this.selectors.getVideoById) {
return video;
}
try {
const stored = this.selectors.getVideoById(video.id);
return stored || video;
} catch (error) {
devLogger.warn(
"[ModerationActionController] Failed to resolve video by id:",
error,
);
return video;
}
}
getCurrentVideo() {
if (!this.selectors.getCurrentVideo) {
return null;
}
try {
return this.selectors.getCurrentVideo();
} catch (error) {
devLogger.warn(
"[ModerationActionController] Failed to resolve current video:",
error,
);
return null;
}
}
clearHideState(video) {
const moderation = this.getModerationState(video);
if (!moderation) {
return;
}
if (moderation.hidden) {
delete moderation.hidden;
}
if (moderation.hideReason) {
delete moderation.hideReason;
}
if (moderation.hideCounts) {
delete moderation.hideCounts;
}
if (moderation.hideBypass) {
delete moderation.hideBypass;
}
}
clearViewerOverride(video) {
const moderation = this.getModerationState(video);
if (moderation?.viewerOverride) {
delete moderation.viewerOverride;
}
}
clearHideBypass(video) {
const moderation = this.getModerationState(video);
if (moderation?.hideBypass) {
delete moderation.hideBypass;
}
}
getModerationState(video) {
if (!video || typeof video !== "object") {
return null;
}
const moderation = video.moderation;
if (!moderation || typeof moderation !== "object") {
return null;
}
return moderation;
}
decorateVideo(video) {
if (!video || !this.actions.decorateVideoModeration) {
return;
}
try {
this.actions.decorateVideoModeration(video);
} catch (error) {
devLogger.warn(
"[ModerationActionController] Failed to decorate video moderation:",
error,
);
}
}
refreshCard(card, { reason } = {}) {
if (!card) {
return;
}
if (this.ui.refreshCardModerationUi) {
this.ui.refreshCardModerationUi(card, { reason });
return;
}
if (typeof card.refreshModerationUi === "function") {
try {
card.refreshModerationUi();
} catch (error) {
const suffix = reason ? ` ${reason}` : "";
devLogger.warn(
`[ModerationActionController] Failed to refresh moderation UI${suffix}:`,
error,
);
}
}
}
dispatchModerationEvent(eventName, detail = {}) {
if (this.ui.dispatchModerationEvent) {
this.ui.dispatchModerationEvent(eventName, detail);
return;
}
const doc =
typeof document !== "undefined" && document
? document
: null;
if (!doc || typeof doc.dispatchEvent !== "function") {
return;
}
try {
doc.dispatchEvent(new CustomEvent(eventName, { detail }));
} catch (error) {
devLogger.warn(
`[ModerationActionController] Failed to dispatch ${eventName}:`,
error,
);
}
}
resumePlayback(video) {
if (!video || !this.actions.resumePlayback) {
return;
}
try {
this.actions.resumePlayback(video);
} catch (error) {
devLogger.warn(
"[ModerationActionController] Failed to resume moderated playback:",
error,
);
}
}
async refreshVideosAfterBlock() {
if (!this.actions.refreshVideos) {
return;
}
try {
await this.actions.refreshVideos({ reason: "user-block-update" });
} catch (error) {
devLogger.warn(
"[ModerationActionController] Failed to refresh videos after block:",
error,
);
}
}
persistModerationOverride(eventId) {
if (!eventId || !this.services.setModerationOverride) {
return;
}
try {
this.services.setModerationOverride(eventId, {
showAnyway: true,
updatedAt: Date.now(),
});
} catch (error) {
devLogger.warn(
"[ModerationActionController] Failed to persist moderation override:",
error,
);
}
}
clearModerationOverride(eventId) {
if (!eventId || !this.services.clearModerationOverride) {
return;
}
try {
this.services.clearModerationOverride(eventId);
} catch (error) {
devLogger.warn(
"[ModerationActionController] Failed to clear moderation override:",
error,
);
}
}
isUserLoggedIn() {
if (!this.auth.isLoggedIn) {
return false;
}
try {
return this.auth.isLoggedIn();
} catch (error) {
devLogger.warn(
"[ModerationActionController] Failed to evaluate login state:",
error,
);
return false;
}
}
getViewerHex() {
if (!this.auth.getViewerPubkey || !this.auth.normalizePubkey) {
return null;
}
try {
const pubkey = this.auth.getViewerPubkey();
return pubkey ? this.auth.normalizePubkey(pubkey) : null;
} catch (error) {
devLogger.warn(
"[ModerationActionController] Failed to resolve viewer pubkey:",
error,
);
return null;
}
}
getTargetHex(video) {
if (!video || !this.auth.normalizePubkey) {
return null;
}
try {
return this.auth.normalizePubkey(video?.pubkey);
} catch (error) {
devLogger.warn(
"[ModerationActionController] Failed to normalize target pubkey:",
error,
);
return null;
}
}
showStatus(message, options) {
if (!this.actions.showStatus) {
return;
}
try {
this.actions.showStatus(message, options);
} catch (error) {
devLogger.warn(
"[ModerationActionController] Failed to show status message:",
error,
);
}
}
showError(message) {
if (!this.actions.showError) {
return;
}
try {
this.actions.showError(message);
} catch (error) {
devLogger.warn(
"[ModerationActionController] Failed to show error message:",
error,
);
}
}
describeBlockError(error) {
if (!this.actions.describeBlockError) {
return "";
}
try {
return this.actions.describeBlockError(error);
} catch (describeError) {
devLogger.warn(
"[ModerationActionController] Failed to describe block error:",
describeError,
);
return "";
}
}
}