mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 08:52:25 +00:00
Merge pull request #940 from PR0M3TH3AN/codex/refactor-videomodal-into-submodules
Refactor video modal into domain controllers
This commit is contained in:
7 files changed
+609
-245
No files matched your search
+41
-245
@@ -4,6 +4,15 @@ import { createVideoMoreMenuPanel } from "./videoMenuRenderers.js";
|
||||
import { attachAmbientBackground } from "../ambientBackground.js";
|
||||
import { applyDesignSystemAttributes } from "../../designSystem.js";
|
||||
import { devLogger } from "../../utils/logger.js";
|
||||
import { CommentsController } from "./video-modal/commentsController.js";
|
||||
import { ReactionsController } from "./video-modal/reactionsController.js";
|
||||
import { SimilarContentController } from "./video-modal/similarContentController.js";
|
||||
import { ModerationController } from "./video-modal/moderationController.js";
|
||||
import {
|
||||
normalizeCommentAvatarKey as normalizeCommentAvatarKeyUtil,
|
||||
resolveCommentAvatarAsset as resolveCommentAvatarAssetUtil,
|
||||
registerCommentAvatarFailure as registerCommentAvatarFailureUtil,
|
||||
} from "./video-modal/utils/commentAvatar.js";
|
||||
import {
|
||||
renderTagPillStrip,
|
||||
applyTagPreferenceState,
|
||||
@@ -188,6 +197,11 @@ export class VideoModal {
|
||||
canManageBlacklist: false,
|
||||
};
|
||||
|
||||
this.commentsController = new CommentsController({ modal: this });
|
||||
this.reactionsController = new ReactionsController({ modal: this });
|
||||
this.similarContentController = new SimilarContentController({ modal: this });
|
||||
this.moderationController = new ModerationController({ modal: this });
|
||||
|
||||
this.commentsRoot = null;
|
||||
this.commentsContainer = null;
|
||||
this.commentsCountLabel = null;
|
||||
@@ -479,6 +493,11 @@ export class VideoModal {
|
||||
);
|
||||
}
|
||||
|
||||
this.commentsController.destroy();
|
||||
this.reactionsController.destroy();
|
||||
this.similarContentController.destroy();
|
||||
this.moderationController.destroy();
|
||||
|
||||
this.clearSimilarContent({ preservePending: true });
|
||||
|
||||
this.playerModal = playerModal;
|
||||
@@ -486,7 +505,6 @@ export class VideoModal {
|
||||
this.modalBackdrop = playerModal.querySelector("[data-dismiss]") || null;
|
||||
this.scrollRegion = this.modalPanel || playerModal;
|
||||
|
||||
this.teardownSimilarContentMediaQuery();
|
||||
const nextVideoTagsRoot = playerModal.querySelector("#videoTags") || null;
|
||||
if (this.videoTagsRoot && this.videoTagsRoot !== nextVideoTagsRoot) {
|
||||
this.cleanupVideoTags(this.videoTagsRoot);
|
||||
@@ -496,26 +514,8 @@ export class VideoModal {
|
||||
this.cleanupVideoTags(this.videoTagsRoot);
|
||||
}
|
||||
|
||||
const similarHeading =
|
||||
playerModal.querySelector("#playerModalSimilarContentHeading") || null;
|
||||
const similarList =
|
||||
playerModal.querySelector("#playerModalSimilarContentList") || null;
|
||||
const similarSection =
|
||||
similarList?.closest(
|
||||
"[aria-labelledby='playerModalSimilarContentHeading']"
|
||||
) || similarHeading?.closest(
|
||||
"[aria-labelledby='playerModalSimilarContentHeading']"
|
||||
) || null;
|
||||
const similarContainer =
|
||||
similarList?.closest(".watch-container") ||
|
||||
similarHeading?.closest(".watch-container") ||
|
||||
similarSection?.closest(".watch-container") ||
|
||||
similarSection ||
|
||||
null;
|
||||
|
||||
this.similarContentHeading = similarHeading;
|
||||
this.similarContentList = similarList;
|
||||
this.similarContentContainer = similarContainer;
|
||||
const { container: similarContainer } =
|
||||
this.similarContentController.initialize({ playerModal }) || {};
|
||||
|
||||
const statsContainerCandidate =
|
||||
playerModal.querySelector("[data-video-stats-container]") ||
|
||||
@@ -540,65 +540,12 @@ export class VideoModal {
|
||||
statsContainer: statsContainerCandidate,
|
||||
});
|
||||
|
||||
this.setupSimilarContentMediaQuery();
|
||||
|
||||
if (similarList && similarList.children.length) {
|
||||
this.toggleSimilarContentVisibility(true);
|
||||
} else {
|
||||
this.toggleSimilarContentVisibility(false);
|
||||
}
|
||||
|
||||
const previousTags = Array.isArray(this.videoTagsData)
|
||||
? [...this.videoTagsData]
|
||||
: [];
|
||||
this.videoTagsData = null;
|
||||
|
||||
this.videoStage = playerModal.querySelector(".video-modal__video") || null;
|
||||
this.moderationOverlay =
|
||||
this.videoStage?.querySelector("[data-moderation-overlay]") || null;
|
||||
this.moderationBadge =
|
||||
this.moderationOverlay?.querySelector("[data-moderation-badge='true']") || null;
|
||||
this.moderationBadgeText =
|
||||
this.moderationOverlay?.querySelector("[data-moderation-text]") || null;
|
||||
if (this.moderationPrimaryButton) {
|
||||
if (this.moderationPrimaryMode === "override") {
|
||||
this.moderationPrimaryButton.removeEventListener(
|
||||
"click",
|
||||
this.handleModerationOverrideClick,
|
||||
);
|
||||
} else if (this.moderationPrimaryMode === "hide") {
|
||||
this.moderationPrimaryButton.removeEventListener(
|
||||
"click",
|
||||
this.handleModerationHideClick,
|
||||
);
|
||||
}
|
||||
}
|
||||
this.moderationActionsContainer =
|
||||
this.moderationOverlay?.querySelector("[data-moderation-actions]") ||
|
||||
this.moderationBadge?.querySelector(".moderation-badge__actions") ||
|
||||
null;
|
||||
const overrideButton =
|
||||
this.moderationActionsContainer?.querySelector(
|
||||
"[data-moderation-action='override']",
|
||||
) || null;
|
||||
if (overrideButton) {
|
||||
overrideButton.addEventListener(
|
||||
"click",
|
||||
this.handleModerationOverrideClick,
|
||||
);
|
||||
this.moderationPrimaryButton = overrideButton;
|
||||
this.moderationPrimaryMode = "override";
|
||||
} else {
|
||||
this.moderationPrimaryButton = null;
|
||||
this.moderationPrimaryMode = "";
|
||||
}
|
||||
if (this.moderationBlockButton) {
|
||||
this.moderationBlockButton.removeEventListener(
|
||||
"click",
|
||||
this.handleModerationBlockClick,
|
||||
);
|
||||
}
|
||||
this.moderationBlockButton = null;
|
||||
this.moderationController.initialize({ playerModal });
|
||||
|
||||
this.modalVideo = playerModal.querySelector("#modalVideo") || null;
|
||||
this.modalStatus = playerModal.querySelector("#modalStatus") || null;
|
||||
@@ -635,22 +582,7 @@ export class VideoModal {
|
||||
this.shareBtn = playerModal.querySelector("#shareBtn") || null;
|
||||
this.modalZapBtn = playerModal.querySelector("#modalZapBtn") || null;
|
||||
this.modalMoreBtn = playerModal.querySelector("#modalMoreBtn") || null;
|
||||
this.reactionButtons["+"] =
|
||||
playerModal.querySelector("#modalLikeBtn") || null;
|
||||
this.reactionButtons["-"] =
|
||||
playerModal.querySelector("#modalDislikeBtn") || null;
|
||||
this.reactionMeter =
|
||||
playerModal.querySelector("[data-reaction-meter]") || null;
|
||||
this.reactionMeterFill =
|
||||
playerModal.querySelector("[data-reaction-meter-fill]") || null;
|
||||
this.reactionMeterLabel =
|
||||
playerModal.querySelector("[data-reaction-meter-label]") || null;
|
||||
this.reactionMeterAssistive =
|
||||
playerModal.querySelector("[data-reaction-meter-sr]") || null;
|
||||
this.reactionCountLabels["+"] =
|
||||
playerModal.querySelector("[data-reaction-like-count]") || null;
|
||||
this.reactionCountLabels["-"] =
|
||||
playerModal.querySelector("[data-reaction-dislike-count]") || null;
|
||||
this.reactionsController.initialize({ playerModal });
|
||||
|
||||
this.renderVideoTags(previousTags);
|
||||
|
||||
@@ -688,90 +620,7 @@ export class VideoModal {
|
||||
|
||||
this.refreshActiveVideoModeration({ video: this.activeVideo });
|
||||
|
||||
this.detachCommentEventHandlers();
|
||||
|
||||
const commentsRoot =
|
||||
playerModal.querySelector("[data-comments-root]") || null;
|
||||
this.commentsRoot = commentsRoot;
|
||||
this.commentsContainer = commentsRoot;
|
||||
this.commentsCountLabel =
|
||||
commentsRoot?.querySelector("[data-comments-count]") || null;
|
||||
this.commentsEmptyState =
|
||||
commentsRoot?.querySelector("[data-comments-empty]") || null;
|
||||
this.commentsList =
|
||||
commentsRoot?.querySelector("[data-comments-list]") || null;
|
||||
this.commentsLoadMoreButton =
|
||||
commentsRoot?.querySelector("[data-comments-load-more]") || null;
|
||||
this.commentsDisabledPlaceholder =
|
||||
playerModal.querySelector("[data-comments-disabled-placeholder]") ||
|
||||
commentsRoot?.querySelector("[data-comments-disabled]") ||
|
||||
null;
|
||||
this.commentsComposer =
|
||||
commentsRoot?.querySelector("[data-comments-composer]") || null;
|
||||
this.commentsInput =
|
||||
this.commentsComposer?.querySelector("[data-comments-input]") || null;
|
||||
this.commentsSubmitButton =
|
||||
this.commentsComposer?.querySelector("[data-comments-submit]") || null;
|
||||
this.commentsStatusMessage =
|
||||
this.commentsComposer?.querySelector("[data-comments-status]") ||
|
||||
commentsRoot?.querySelector("[data-comments-status]") ||
|
||||
null;
|
||||
this.commentsCharCount =
|
||||
this.commentsComposer?.querySelector("[data-comments-char-count]") ||
|
||||
null;
|
||||
this.commentComposerHint =
|
||||
this.commentsComposer?.querySelector("#commentComposerHelp") ||
|
||||
this.commentsComposer?.querySelector(".comment-composer__hint") ||
|
||||
this.commentComposerHint ||
|
||||
null;
|
||||
|
||||
if (this.commentComposerHint) {
|
||||
const hintText = this.commentComposerHint.textContent || "";
|
||||
if (hintText) {
|
||||
this.commentComposerDefaultHint = hintText;
|
||||
}
|
||||
}
|
||||
if (this.commentsCharCount) {
|
||||
const countText = this.commentsCharCount.textContent || "";
|
||||
if (countText) {
|
||||
this.commentComposerDefaultCountText = countText;
|
||||
}
|
||||
}
|
||||
if (this.commentsStatusMessage) {
|
||||
const statusText = this.commentsStatusMessage.textContent || "";
|
||||
if (statusText) {
|
||||
this.commentStatusDefaultText = statusText;
|
||||
}
|
||||
}
|
||||
if (this.commentsInput) {
|
||||
const parsedMax = Number(this.commentsInput.maxLength);
|
||||
if (Number.isFinite(parsedMax) && parsedMax > 0) {
|
||||
this.commentComposerMaxLength = parsedMax;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.commentsDisabledPlaceholder) {
|
||||
const disabledText = this.commentsDisabledPlaceholder.textContent || "";
|
||||
if (disabledText) {
|
||||
this.commentsDisabledPlaceholderDefaultText = disabledText;
|
||||
}
|
||||
this.commentsDisabledPlaceholder.setAttribute("hidden", "");
|
||||
}
|
||||
if (this.commentsComposer) {
|
||||
this.commentsComposer.removeAttribute("hidden");
|
||||
}
|
||||
|
||||
this.clearComments();
|
||||
this.resetCommentComposer();
|
||||
this.attachCommentEventHandlers();
|
||||
|
||||
Object.values(this.reactionButtons).forEach((button) => {
|
||||
if (button) {
|
||||
button.addEventListener("click", this.handleReactionClick);
|
||||
}
|
||||
});
|
||||
this.syncReactionButtons();
|
||||
this.updateReactionMeterDisplay();
|
||||
this.commentsController.initialize({ playerModal });
|
||||
|
||||
const closeButton = playerModal.querySelector("#closeModal");
|
||||
if (closeButton) {
|
||||
@@ -1773,80 +1622,25 @@ export class VideoModal {
|
||||
}
|
||||
|
||||
resolveCommentAvatarAsset(pubkey, sanitizedPicture) {
|
||||
const normalizedPubkey = this.normalizeCommentAvatarKey(pubkey);
|
||||
const normalizedSource =
|
||||
typeof sanitizedPicture === "string" && sanitizedPicture
|
||||
? sanitizedPicture
|
||||
: "";
|
||||
|
||||
if (normalizedSource && this.commentAvatarFailures.has(normalizedSource)) {
|
||||
return { url: this.DEFAULT_PROFILE_AVATAR, source: "" };
|
||||
}
|
||||
|
||||
if (normalizedPubkey && this.commentAvatarCache.has(normalizedPubkey)) {
|
||||
const cached = this.commentAvatarCache.get(normalizedPubkey);
|
||||
if (cached) {
|
||||
const cachedSource =
|
||||
typeof cached.source === "string" ? cached.source : "";
|
||||
const cachedUrl =
|
||||
typeof cached.url === "string" && cached.url
|
||||
? cached.url
|
||||
: this.DEFAULT_PROFILE_AVATAR;
|
||||
|
||||
if (!normalizedSource) {
|
||||
return { url: cachedUrl, source: cachedSource };
|
||||
}
|
||||
|
||||
if (cachedSource === normalizedSource) {
|
||||
return { url: cachedUrl, source: cachedSource };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedUrl = normalizedSource || this.DEFAULT_PROFILE_AVATAR;
|
||||
if (normalizedPubkey) {
|
||||
this.commentAvatarCache.set(normalizedPubkey, {
|
||||
url: resolvedUrl,
|
||||
source: normalizedSource,
|
||||
});
|
||||
}
|
||||
|
||||
return { url: resolvedUrl, source: normalizedSource };
|
||||
return resolveCommentAvatarAssetUtil({
|
||||
cache: this.commentAvatarCache,
|
||||
failures: this.commentAvatarFailures,
|
||||
defaultAvatar: this.DEFAULT_PROFILE_AVATAR,
|
||||
pubkey,
|
||||
sanitizedPicture,
|
||||
});
|
||||
}
|
||||
|
||||
normalizeCommentAvatarKey(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
return trimmed.toLowerCase();
|
||||
return normalizeCommentAvatarKeyUtil(value);
|
||||
}
|
||||
|
||||
registerCommentAvatarFailure(sourceUrl) {
|
||||
if (typeof sourceUrl !== "string") {
|
||||
return;
|
||||
}
|
||||
const trimmed = sourceUrl.trim();
|
||||
if (!trimmed || trimmed === this.DEFAULT_PROFILE_AVATAR) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.commentAvatarFailures.has(trimmed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.commentAvatarFailures.add(trimmed);
|
||||
|
||||
this.commentAvatarCache.forEach((entry, key) => {
|
||||
if (entry && entry.source === trimmed) {
|
||||
this.commentAvatarCache.set(key, {
|
||||
url: this.DEFAULT_PROFILE_AVATAR,
|
||||
source: "",
|
||||
});
|
||||
}
|
||||
registerCommentAvatarFailureUtil({
|
||||
cache: this.commentAvatarCache,
|
||||
failures: this.commentAvatarFailures,
|
||||
defaultAvatar: this.DEFAULT_PROFILE_AVATAR,
|
||||
sourceUrl,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2136,8 +1930,10 @@ export class VideoModal {
|
||||
|
||||
destroy() {
|
||||
this.close();
|
||||
this.detachCommentEventHandlers();
|
||||
this.removeCommentRetryButton();
|
||||
this.commentsController.destroy();
|
||||
this.reactionsController.destroy();
|
||||
this.similarContentController.destroy();
|
||||
this.moderationController.destroy();
|
||||
this.invokeCommentTeardown();
|
||||
|
||||
if (this.document) {
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
export class CommentsController {
|
||||
constructor({ modal } = {}) {
|
||||
this.modal = modal;
|
||||
this.root = null;
|
||||
}
|
||||
|
||||
initialize({ playerModal } = {}) {
|
||||
if (!this.modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const root = playerModal?.querySelector("[data-comments-root]") || null;
|
||||
this.root = root;
|
||||
this.modal.commentsRoot = root;
|
||||
this.modal.commentsContainer = root;
|
||||
this.modal.commentsCountLabel =
|
||||
root?.querySelector("[data-comments-count]") || null;
|
||||
this.modal.commentsEmptyState =
|
||||
root?.querySelector("[data-comments-empty]") || null;
|
||||
this.modal.commentsList =
|
||||
root?.querySelector("[data-comments-list]") || null;
|
||||
this.modal.commentsLoadMoreButton =
|
||||
root?.querySelector("[data-comments-load-more]") || null;
|
||||
this.modal.commentsDisabledPlaceholder =
|
||||
playerModal?.querySelector("[data-comments-disabled-placeholder]") ||
|
||||
root?.querySelector("[data-comments-disabled]") ||
|
||||
null;
|
||||
this.modal.commentsComposer =
|
||||
root?.querySelector("[data-comments-composer]") || null;
|
||||
this.modal.commentsInput =
|
||||
this.modal.commentsComposer?.querySelector("[data-comments-input]") || null;
|
||||
this.modal.commentsSubmitButton =
|
||||
this.modal.commentsComposer?.querySelector("[data-comments-submit]") || null;
|
||||
this.modal.commentsStatusMessage =
|
||||
this.modal.commentsComposer?.querySelector("[data-comments-status]") ||
|
||||
root?.querySelector("[data-comments-status]") ||
|
||||
null;
|
||||
this.modal.commentsCharCount =
|
||||
this.modal.commentsComposer?.querySelector("[data-comments-char-count]") ||
|
||||
null;
|
||||
this.modal.commentComposerHint =
|
||||
this.modal.commentsComposer?.querySelector("#commentComposerHelp") ||
|
||||
this.modal.commentsComposer?.querySelector(".comment-composer__hint") ||
|
||||
this.modal.commentComposerHint ||
|
||||
null;
|
||||
|
||||
if (this.modal.commentComposerHint) {
|
||||
const hintText = this.modal.commentComposerHint.textContent || "";
|
||||
if (hintText) {
|
||||
this.modal.commentComposerDefaultHint = hintText;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.modal.commentsCharCount) {
|
||||
const countText = this.modal.commentsCharCount.textContent || "";
|
||||
if (countText) {
|
||||
this.modal.commentComposerDefaultCountText = countText;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.modal.commentsStatusMessage) {
|
||||
const statusText = this.modal.commentsStatusMessage.textContent || "";
|
||||
if (statusText) {
|
||||
this.modal.commentStatusDefaultText = statusText;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.modal.commentsInput) {
|
||||
const parsedMax = Number(this.modal.commentsInput.maxLength);
|
||||
if (Number.isFinite(parsedMax) && parsedMax > 0) {
|
||||
this.modal.commentComposerMaxLength = parsedMax;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.modal.commentsDisabledPlaceholder) {
|
||||
const disabledText =
|
||||
this.modal.commentsDisabledPlaceholder.textContent || "";
|
||||
if (disabledText) {
|
||||
this.modal.commentsDisabledPlaceholderDefaultText = disabledText;
|
||||
}
|
||||
this.modal.commentsDisabledPlaceholder.setAttribute("hidden", "");
|
||||
}
|
||||
|
||||
if (this.modal.commentsComposer) {
|
||||
this.modal.commentsComposer.removeAttribute("hidden");
|
||||
}
|
||||
|
||||
this.modal.clearComments();
|
||||
this.modal.resetCommentComposer();
|
||||
this.modal.attachCommentEventHandlers();
|
||||
}
|
||||
|
||||
update(action = {}) {
|
||||
if (!this.modal) {
|
||||
return;
|
||||
}
|
||||
const { type } = action;
|
||||
switch (type) {
|
||||
case "set-visibility":
|
||||
this.modal.setCommentsVisibility(action.visible);
|
||||
break;
|
||||
case "render":
|
||||
this.modal.renderComments(action.snapshot);
|
||||
break;
|
||||
case "set-composer-state":
|
||||
this.modal.setCommentComposerState(action.state);
|
||||
break;
|
||||
case "set-thread-context":
|
||||
this.modal.setCommentThreadContext(action.context);
|
||||
break;
|
||||
case "clear":
|
||||
this.modal.clearComments();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.modal) {
|
||||
return;
|
||||
}
|
||||
this.modal.detachCommentEventHandlers();
|
||||
this.modal.removeCommentRetryButton();
|
||||
this.modal.commentsRoot = null;
|
||||
this.modal.commentsContainer = null;
|
||||
this.modal.commentsCountLabel = null;
|
||||
this.modal.commentsEmptyState = null;
|
||||
this.modal.commentsList = null;
|
||||
this.modal.commentsLoadMoreButton = null;
|
||||
this.modal.commentsDisabledPlaceholder = null;
|
||||
this.modal.commentsComposer = null;
|
||||
this.modal.commentsInput = null;
|
||||
this.modal.commentsSubmitButton = null;
|
||||
this.modal.commentsStatusMessage = null;
|
||||
this.modal.commentsCharCount = null;
|
||||
this.modal.commentComposerHint = null;
|
||||
this.root = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
export class ModerationController {
|
||||
constructor({ modal } = {}) {
|
||||
this.modal = modal;
|
||||
}
|
||||
|
||||
initialize({ playerModal } = {}) {
|
||||
if (!this.modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.modal.videoStage =
|
||||
playerModal?.querySelector(".video-modal__video") || null;
|
||||
this.modal.moderationOverlay =
|
||||
this.modal.videoStage?.querySelector("[data-moderation-overlay]") || null;
|
||||
this.modal.moderationBadge =
|
||||
this.modal.moderationOverlay?.querySelector(
|
||||
"[data-moderation-badge='true']",
|
||||
) || null;
|
||||
this.modal.moderationBadgeText =
|
||||
this.modal.moderationOverlay?.querySelector("[data-moderation-text]") ||
|
||||
null;
|
||||
|
||||
this.teardownPrimaryButton();
|
||||
this.modal.moderationActionsContainer =
|
||||
this.modal.moderationOverlay?.querySelector("[data-moderation-actions]") ||
|
||||
this.modal.moderationBadge?.querySelector(".moderation-badge__actions") ||
|
||||
null;
|
||||
|
||||
const overrideButton =
|
||||
this.modal.moderationActionsContainer?.querySelector(
|
||||
"[data-moderation-action='override']",
|
||||
) || null;
|
||||
if (overrideButton) {
|
||||
overrideButton.addEventListener(
|
||||
"click",
|
||||
this.modal.handleModerationOverrideClick,
|
||||
);
|
||||
this.modal.moderationPrimaryButton = overrideButton;
|
||||
this.modal.moderationPrimaryMode = "override";
|
||||
} else {
|
||||
this.modal.moderationPrimaryButton = null;
|
||||
this.modal.moderationPrimaryMode = "";
|
||||
}
|
||||
|
||||
this.teardownBlockButton();
|
||||
const blockButton =
|
||||
this.modal.moderationActionsContainer?.querySelector(
|
||||
"[data-moderation-action='block']",
|
||||
) || null;
|
||||
if (blockButton) {
|
||||
blockButton.addEventListener(
|
||||
"click",
|
||||
this.modal.handleModerationBlockClick,
|
||||
);
|
||||
this.modal.moderationBlockButton = blockButton;
|
||||
} else {
|
||||
this.modal.moderationBlockButton = null;
|
||||
}
|
||||
}
|
||||
|
||||
update(action = {}) {
|
||||
if (!this.modal) {
|
||||
return;
|
||||
}
|
||||
const { type } = action;
|
||||
switch (type) {
|
||||
case "set-context":
|
||||
this.modal.refreshActiveVideoModeration?.(action.context);
|
||||
break;
|
||||
case "set-overlay":
|
||||
this.modal.applyModerationOverlay?.(action.overlay);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.modal) {
|
||||
return;
|
||||
}
|
||||
this.teardownPrimaryButton();
|
||||
this.teardownBlockButton();
|
||||
this.modal.moderationOverlay = null;
|
||||
this.modal.moderationBadge = null;
|
||||
this.modal.moderationBadgeText = null;
|
||||
this.modal.moderationActionsContainer = null;
|
||||
this.modal.moderationPrimaryButton = null;
|
||||
this.modal.moderationPrimaryMode = "";
|
||||
this.modal.moderationBlockButton = null;
|
||||
}
|
||||
|
||||
teardownPrimaryButton() {
|
||||
if (!this.modal?.moderationPrimaryButton) {
|
||||
return;
|
||||
}
|
||||
const button = this.modal.moderationPrimaryButton;
|
||||
if (this.modal.moderationPrimaryMode === "override") {
|
||||
button.removeEventListener(
|
||||
"click",
|
||||
this.modal.handleModerationOverrideClick,
|
||||
);
|
||||
} else if (this.modal.moderationPrimaryMode === "hide") {
|
||||
button.removeEventListener("click", this.modal.handleModerationHideClick);
|
||||
}
|
||||
}
|
||||
|
||||
teardownBlockButton() {
|
||||
if (!this.modal?.moderationBlockButton) {
|
||||
return;
|
||||
}
|
||||
this.modal.moderationBlockButton.removeEventListener(
|
||||
"click",
|
||||
this.modal.handleModerationBlockClick,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
export class ReactionsController {
|
||||
constructor({ modal } = {}) {
|
||||
this.modal = modal;
|
||||
this.boundHandler = (event) => {
|
||||
this.modal?.handleReactionClick?.(event);
|
||||
};
|
||||
this.buttons = [];
|
||||
}
|
||||
|
||||
initialize({ playerModal } = {}) {
|
||||
if (!this.modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const likeButton = playerModal?.querySelector("#modalLikeBtn") || null;
|
||||
const dislikeButton = playerModal?.querySelector("#modalDislikeBtn") || null;
|
||||
this.modal.reactionButtons = { "+": likeButton, "-": dislikeButton };
|
||||
this.modal.reactionMeter =
|
||||
playerModal?.querySelector("[data-reaction-meter]") || null;
|
||||
this.modal.reactionMeterFill =
|
||||
playerModal?.querySelector("[data-reaction-meter-fill]") || null;
|
||||
this.modal.reactionMeterLabel =
|
||||
playerModal?.querySelector("[data-reaction-meter-label]") || null;
|
||||
this.modal.reactionMeterAssistive =
|
||||
playerModal?.querySelector("[data-reaction-meter-sr]") || null;
|
||||
this.modal.reactionCountLabels = {
|
||||
"+": playerModal?.querySelector("[data-reaction-like-count]") || null,
|
||||
"-": playerModal?.querySelector("[data-reaction-dislike-count]") || null,
|
||||
};
|
||||
|
||||
this.buttons = [likeButton, dislikeButton].filter(Boolean);
|
||||
this.buttons.forEach((button) => {
|
||||
button.removeEventListener("click", this.boundHandler);
|
||||
button.addEventListener("click", this.boundHandler);
|
||||
});
|
||||
|
||||
this.modal.syncReactionButtons?.();
|
||||
this.modal.updateReactionMeterDisplay?.();
|
||||
}
|
||||
|
||||
update(action = {}) {
|
||||
if (!this.modal) {
|
||||
return;
|
||||
}
|
||||
const { type } = action;
|
||||
switch (type) {
|
||||
case "set-summary":
|
||||
this.modal.updateReactionSummary?.(action.summary);
|
||||
break;
|
||||
case "set-user-reaction":
|
||||
this.modal.setUserReaction?.(action.reaction);
|
||||
break;
|
||||
case "reset":
|
||||
this.modal.resetReactions?.();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.buttons.forEach((button) => {
|
||||
button.removeEventListener("click", this.boundHandler);
|
||||
});
|
||||
this.buttons = [];
|
||||
if (this.modal) {
|
||||
this.modal.reactionButtons = { "+": null, "-": null };
|
||||
this.modal.reactionMeter = null;
|
||||
this.modal.reactionMeterFill = null;
|
||||
this.modal.reactionMeterLabel = null;
|
||||
this.modal.reactionMeterAssistive = null;
|
||||
this.modal.reactionCountLabels = { "+": null, "-": null };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
export class SimilarContentController {
|
||||
constructor({ modal } = {}) {
|
||||
this.modal = modal;
|
||||
}
|
||||
|
||||
initialize({ playerModal } = {}) {
|
||||
if (!this.modal) {
|
||||
return { container: null, heading: null, list: null };
|
||||
}
|
||||
|
||||
this.modal.teardownSimilarContentMediaQuery?.();
|
||||
|
||||
const heading =
|
||||
playerModal?.querySelector("#playerModalSimilarContentHeading") || null;
|
||||
const list =
|
||||
playerModal?.querySelector("#playerModalSimilarContentList") || null;
|
||||
const section =
|
||||
list?.closest("[aria-labelledby='playerModalSimilarContentHeading']") ||
|
||||
heading?.closest("[aria-labelledby='playerModalSimilarContentHeading']") ||
|
||||
null;
|
||||
const container =
|
||||
list?.closest(".watch-container") ||
|
||||
heading?.closest(".watch-container") ||
|
||||
section?.closest(".watch-container") ||
|
||||
section ||
|
||||
null;
|
||||
|
||||
this.modal.similarContentHeading = heading;
|
||||
this.modal.similarContentList = list;
|
||||
this.modal.similarContentContainer = container;
|
||||
|
||||
this.modal.setupSimilarContentMediaQuery?.();
|
||||
|
||||
if (list && list.children.length) {
|
||||
this.modal.toggleSimilarContentVisibility?.(true);
|
||||
} else {
|
||||
this.modal.toggleSimilarContentVisibility?.(false);
|
||||
}
|
||||
|
||||
return { container, heading, list };
|
||||
}
|
||||
|
||||
update(action = {}) {
|
||||
if (!this.modal) {
|
||||
return;
|
||||
}
|
||||
const { type } = action;
|
||||
switch (type) {
|
||||
case "set-items":
|
||||
this.modal.setSimilarContent?.(action.items, action.options);
|
||||
break;
|
||||
case "set-visibility":
|
||||
this.modal.toggleSimilarContentVisibility?.(action.visible);
|
||||
break;
|
||||
case "clear":
|
||||
this.modal.clearSimilarContent?.(action.options);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.modal) {
|
||||
return;
|
||||
}
|
||||
this.modal.teardownSimilarContentMediaQuery?.();
|
||||
this.modal.similarContentHeading = null;
|
||||
this.modal.similarContentList = null;
|
||||
this.modal.similarContentContainer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
export function normalizeCommentAvatarKey(value) {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
export function resolveCommentAvatarAsset({
|
||||
cache,
|
||||
failures,
|
||||
defaultAvatar,
|
||||
pubkey,
|
||||
sanitizedPicture,
|
||||
}) {
|
||||
const normalizedPubkey = normalizeCommentAvatarKey(pubkey);
|
||||
const normalizedSource =
|
||||
typeof sanitizedPicture === "string" && sanitizedPicture
|
||||
? sanitizedPicture
|
||||
: "";
|
||||
|
||||
if (normalizedSource && failures?.has?.(normalizedSource)) {
|
||||
return { url: defaultAvatar, source: "" };
|
||||
}
|
||||
|
||||
if (normalizedPubkey && cache?.has?.(normalizedPubkey)) {
|
||||
const cached = cache.get(normalizedPubkey);
|
||||
if (cached) {
|
||||
const cachedSource =
|
||||
typeof cached.source === "string" ? cached.source : "";
|
||||
const cachedUrl =
|
||||
typeof cached.url === "string" && cached.url
|
||||
? cached.url
|
||||
: defaultAvatar;
|
||||
|
||||
if (!normalizedSource || cachedSource === normalizedSource) {
|
||||
return { url: cachedUrl, source: cachedSource };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedUrl = normalizedSource || defaultAvatar;
|
||||
if (normalizedPubkey && cache) {
|
||||
cache.set(normalizedPubkey, { url: resolvedUrl, source: normalizedSource });
|
||||
}
|
||||
|
||||
return { url: resolvedUrl, source: normalizedSource };
|
||||
}
|
||||
|
||||
export function registerCommentAvatarFailure({
|
||||
cache,
|
||||
failures,
|
||||
defaultAvatar,
|
||||
sourceUrl,
|
||||
}) {
|
||||
if (typeof sourceUrl !== "string") {
|
||||
return;
|
||||
}
|
||||
const trimmed = sourceUrl.trim();
|
||||
if (!trimmed || trimmed === defaultAvatar) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!failures?.has?.(trimmed)) {
|
||||
failures?.add?.(trimmed);
|
||||
}
|
||||
|
||||
if (!cache?.forEach) {
|
||||
return;
|
||||
}
|
||||
|
||||
cache.forEach((entry, key) => {
|
||||
if (entry && entry.source === trimmed) {
|
||||
cache.set(key, { url: defaultAvatar, source: "" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { setupModal } from "./video-modal-accessibility.test.mjs";
|
||||
|
||||
test("CommentsController lifecycle manages comment references", async (t) => {
|
||||
const { modal, cleanup } = await setupModal();
|
||||
t.after(cleanup);
|
||||
|
||||
assert.ok(modal.commentsRoot, "comments should be initialized after hydrate");
|
||||
|
||||
modal.commentsController.destroy();
|
||||
assert.equal(modal.commentsRoot, null, "destroy should clear comment root");
|
||||
|
||||
modal.commentsController.initialize({ playerModal: modal.getRoot() });
|
||||
assert.ok(modal.commentsRoot, "initialize should restore comment root");
|
||||
});
|
||||
|
||||
test("ReactionsController delegates reaction updates", async (t) => {
|
||||
const { modal, cleanup } = await setupModal();
|
||||
t.after(cleanup);
|
||||
|
||||
modal.reactionsController.update({ type: "set-user-reaction", reaction: "+" });
|
||||
assert.equal(
|
||||
modal.reactionState.userReaction,
|
||||
"+",
|
||||
"user reaction should be updated via controller",
|
||||
);
|
||||
|
||||
modal.reactionsController.destroy();
|
||||
assert.equal(
|
||||
modal.reactionButtons["+"],
|
||||
null,
|
||||
"destroy should clear reaction button references",
|
||||
);
|
||||
});
|
||||
|
||||
test("SimilarContentController toggles section visibility", async (t) => {
|
||||
const { modal, cleanup } = await setupModal();
|
||||
t.after(cleanup);
|
||||
|
||||
const container = modal.similarContentContainer;
|
||||
assert.ok(container, "similar content container should exist");
|
||||
|
||||
modal.similarContentController.update({ type: "set-visibility", visible: false });
|
||||
assert.equal(
|
||||
container.hasAttribute("hidden"),
|
||||
true,
|
||||
"controller should hide similar content when requested",
|
||||
);
|
||||
|
||||
modal.similarContentController.update({ type: "set-visibility", visible: true });
|
||||
assert.equal(
|
||||
container.hasAttribute("hidden"),
|
||||
false,
|
||||
"controller should reveal similar content",
|
||||
);
|
||||
});
|
||||
|
||||
test("ModerationController reset clears moderation overlay references", async (t) => {
|
||||
const { modal, cleanup } = await setupModal();
|
||||
t.after(cleanup);
|
||||
|
||||
assert.ok(modal.moderationOverlay, "moderation overlay should be present");
|
||||
|
||||
modal.moderationController.destroy();
|
||||
assert.equal(
|
||||
modal.moderationOverlay,
|
||||
null,
|
||||
"destroy should clear moderation overlay references",
|
||||
);
|
||||
|
||||
modal.moderationController.initialize({ playerModal: modal.getRoot() });
|
||||
assert.ok(
|
||||
modal.moderationOverlay,
|
||||
"initialize should restore moderation overlay references",
|
||||
);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user