mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
327 lines
9.8 KiB
JavaScript
327 lines
9.8 KiB
JavaScript
// js/app/moderationCoordinator.js
|
|
|
|
/**
|
|
* Moderation setting update flow, decoration, and action routing.
|
|
*
|
|
* All module-level dependencies are injected from the Application
|
|
* composition root rather than imported at module scope.
|
|
*
|
|
* Methods use `this` which is bound to the Application instance.
|
|
*/
|
|
|
|
/**
|
|
* @param {object} deps - Injected dependencies.
|
|
* @returns {object} Methods to be bound to the Application instance.
|
|
*/
|
|
export function createModerationCoordinator(deps) {
|
|
const {
|
|
devLogger,
|
|
ModerationActionController,
|
|
setModerationOverride,
|
|
clearModerationOverride,
|
|
clearAuthorModerationOverride,
|
|
userBlocks,
|
|
buildVideoAddressPointer,
|
|
VIDEO_EVENT_KIND,
|
|
} = deps;
|
|
|
|
return {
|
|
async handleModerationSettingsChange({ settings, skipRefresh = false } = {}) {
|
|
const normalized = this.normalizeModerationSettings(settings);
|
|
this.moderationSettings = normalized;
|
|
const feedContext = {
|
|
feedName: this.feedName || "",
|
|
feedVariant: this.feedVariant || "",
|
|
};
|
|
|
|
if (this.videosMap instanceof Map) {
|
|
for (const video of this.videosMap.values()) {
|
|
if (video && typeof video === "object") {
|
|
this.decorateVideoModeration(video, feedContext);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
this.videoListView &&
|
|
Array.isArray(this.videoListView.videoCardInstances)
|
|
) {
|
|
for (const card of this.videoListView.videoCardInstances) {
|
|
if (!card || typeof card.refreshModerationUi !== "function") {
|
|
continue;
|
|
}
|
|
if (card.video && typeof card.video === "object") {
|
|
this.decorateVideoModeration(card.video, feedContext);
|
|
}
|
|
try {
|
|
card.refreshModerationUi();
|
|
} catch (error) {
|
|
devLogger.warn(
|
|
"[Application] Failed to refresh moderation UI:",
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.videoListView && Array.isArray(this.videoListView.currentVideos)) {
|
|
for (const video of this.videoListView.currentVideos) {
|
|
if (video && typeof video === "object") {
|
|
this.decorateVideoModeration(video, feedContext);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.currentVideo && typeof this.currentVideo === "object") {
|
|
this.decorateVideoModeration(this.currentVideo, feedContext);
|
|
}
|
|
|
|
this.moderationDecorator.updateSettings(normalized);
|
|
|
|
if (!skipRefresh) {
|
|
try {
|
|
await this.onVideosShouldRefresh({ reason: "moderation-settings-change" });
|
|
} catch (error) {
|
|
devLogger.warn(
|
|
"[Application] Failed to refresh videos after moderation settings change:",
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
return normalized;
|
|
},
|
|
|
|
refreshVisibleModerationUi({ reason } = {}) {
|
|
const context = reason ? ` after ${reason}` : "";
|
|
const feedContext = {
|
|
feedName: this.feedName || "",
|
|
feedVariant: this.feedVariant || "",
|
|
};
|
|
|
|
const redecorateVideo = (video) => {
|
|
if (!video || typeof video !== "object") {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.decorateVideoModeration(video, feedContext);
|
|
} catch (error) {
|
|
devLogger.warn(
|
|
`[Application] Failed to decorate video moderation${context}:`,
|
|
error,
|
|
);
|
|
}
|
|
};
|
|
|
|
if (this.videosMap instanceof Map) {
|
|
for (const video of this.videosMap.values()) {
|
|
redecorateVideo(video);
|
|
}
|
|
}
|
|
|
|
if (this.videoListView && Array.isArray(this.videoListView.currentVideos)) {
|
|
for (const video of this.videoListView.currentVideos) {
|
|
redecorateVideo(video);
|
|
}
|
|
}
|
|
|
|
if (this.videoListView && Array.isArray(this.videoListView.videoCardInstances)) {
|
|
for (const card of this.videoListView.videoCardInstances) {
|
|
if (!card || typeof card !== "object") {
|
|
continue;
|
|
}
|
|
|
|
if (card.video && typeof card.video === "object") {
|
|
redecorateVideo(card.video);
|
|
}
|
|
|
|
if (typeof card.refreshModerationUi === "function") {
|
|
try {
|
|
card.refreshModerationUi();
|
|
} catch (error) {
|
|
devLogger.warn(
|
|
`[Application] Failed to refresh moderation UI on card${context}:`,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.currentVideo && typeof this.currentVideo === "object") {
|
|
redecorateVideo(this.currentVideo);
|
|
|
|
try {
|
|
this.videoModal?.refreshActiveVideoModeration?.({ video: this.currentVideo });
|
|
} catch (error) {
|
|
devLogger.warn(
|
|
`[Application] Failed to refresh video modal moderation UI${context}:`,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
},
|
|
|
|
deriveModerationReportType(summary) {
|
|
return this.moderationDecorator.deriveModerationReportType(summary);
|
|
},
|
|
|
|
deriveModerationTrustedCount(summary, reportType) {
|
|
return this.moderationDecorator.deriveModerationTrustedCount(summary, reportType);
|
|
},
|
|
|
|
getReporterDisplayName(pubkey) {
|
|
return this.moderationDecorator.getReporterDisplayName(pubkey);
|
|
},
|
|
|
|
normalizeModerationSettings(settings = null) {
|
|
return this.moderationDecorator.normalizeModerationSettings(settings);
|
|
},
|
|
|
|
getActiveModerationThresholds() {
|
|
if (!this.moderationSettings || typeof this.moderationSettings !== "object") {
|
|
this.moderationSettings = this.moderationDecorator.normalizeModerationSettings(this.moderationSettings);
|
|
}
|
|
return this.moderationSettings;
|
|
},
|
|
|
|
decorateVideoModeration(video, feedContext = {}) {
|
|
const decorated = this.moderationDecorator.decorateVideo(video, feedContext);
|
|
if (
|
|
video &&
|
|
video.pubkey &&
|
|
this.isAuthorBlocked(video.pubkey) &&
|
|
decorated &&
|
|
decorated.moderation
|
|
) {
|
|
decorated.moderation.viewerMuted = true;
|
|
decorated.moderation.hidden = true;
|
|
decorated.moderation.hideReason = "viewer-block";
|
|
}
|
|
return decorated;
|
|
},
|
|
|
|
initializeModerationActionController() {
|
|
if (this.moderationActionController) {
|
|
return this.moderationActionController;
|
|
}
|
|
|
|
this.moderationActionController = new ModerationActionController({
|
|
services: {
|
|
setModerationOverride,
|
|
clearModerationOverride,
|
|
clearAuthorModerationOverride,
|
|
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),
|
|
},
|
|
});
|
|
|
|
return this.moderationActionController;
|
|
},
|
|
|
|
refreshCardModerationUi(card, { reason } = {}) {
|
|
if (!card || typeof card.refreshModerationUi !== "function") {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
card.refreshModerationUi();
|
|
return true;
|
|
} catch (error) {
|
|
const suffix = reason ? ` ${reason}` : "";
|
|
devLogger.warn(
|
|
`[Application] Failed to refresh moderation UI${suffix}:`,
|
|
error,
|
|
);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
dispatchModerationEvent(eventName, detail = {}) {
|
|
const doc =
|
|
(this.videoModal && this.videoModal.document) ||
|
|
(typeof document !== "undefined" ? document : null);
|
|
|
|
if (!doc || typeof doc.dispatchEvent !== "function") {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
doc.dispatchEvent(new CustomEvent(eventName, { detail }));
|
|
return true;
|
|
} catch (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;
|
|
}
|
|
|
|
return controller.handleOverride(payload);
|
|
},
|
|
|
|
async handleModerationBlock(payload = {}) {
|
|
const controller = this.initializeModerationActionController();
|
|
if (!controller) {
|
|
return false;
|
|
}
|
|
|
|
return controller.handleBlock(payload);
|
|
},
|
|
|
|
handleModerationHide(payload = {}) {
|
|
const controller = this.initializeModerationActionController();
|
|
if (!controller) {
|
|
return false;
|
|
}
|
|
|
|
return controller.handleHide(payload);
|
|
},
|
|
|
|
getVideoAddressPointer(video) {
|
|
if (
|
|
this.discussionCountService &&
|
|
typeof this.discussionCountService.getVideoAddressPointer === "function"
|
|
) {
|
|
return this.discussionCountService.getVideoAddressPointer(video);
|
|
}
|
|
|
|
return buildVideoAddressPointer(video, { defaultKind: VIDEO_EVENT_KIND });
|
|
},
|
|
};
|
|
}
|