Merge pull request #2735 from PR0M3TH3AN/refactor-videomodal-comments-13431359782891705470

🧹 Refactor VideoModal to extract CommentsController logic
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-26 13:00:27 -05:00
commit c60eada1a6
4 files changed
+1547 -2158

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,402 @@
import { UI_FEEDBACK_DELAY_MS } from "../../../constants.js";
import { formatAbsoluteTimestamp, formatTimeAgo } from "../../../utils/formatters.js";
import { registerCommentAvatarFailure as registerCommentAvatarFailureUtil } from "./utils/commentAvatar.js";
export class CommentNodeFactory {
constructor({ document, window, logger, dispatch, DEFAULT_PROFILE_AVATAR }) {
this.document = document;
this.window = window;
this.logger = logger;
this.dispatch = dispatch;
this.DEFAULT_PROFILE_AVATAR = DEFAULT_PROFILE_AVATAR;
}
createCommentNode(event, { depth = 0 } = {}) {
if (!event || typeof event !== "object" || !this.document) {
return null;
}
const commentId =
typeof event.id === "string" && event.id.trim() ? event.id.trim() : "";
if (!commentId) {
return null;
}
const listItem = this.document.createElement("li");
listItem.classList.add("comment-thread__item");
listItem.dataset.commentId = commentId;
listItem.dataset.commentDepth = String(Math.max(0, depth));
listItem.dataset.timestamp = String(
Number.isFinite(event.created_at) ? event.created_at : 0
);
if (typeof event.pubkey === "string" && event.pubkey.trim()) {
listItem.dataset.commentAuthor = event.pubkey.trim();
}
const avatarWrapper = this.document.createElement("div");
avatarWrapper.classList.add("comment-thread__avatar");
const avatarImg = this.document.createElement("img");
avatarImg.hidden = true;
avatarImg.loading = "lazy";
avatarImg.decoding = "async";
avatarWrapper.appendChild(avatarImg);
const avatarLabel = this.document.createElement("span");
avatarWrapper.appendChild(avatarLabel);
const content = this.document.createElement("div");
content.classList.add("comment-thread__content");
const meta = this.document.createElement("div");
meta.classList.add("comment-thread__meta");
const authorEl = this.document.createElement("span");
authorEl.classList.add("comment-thread__author");
meta.appendChild(authorEl);
const timestamp = this.getCommentTimestampLabel(event.created_at);
if (timestamp.text) {
const timeEl = this.document.createElement("time");
timeEl.classList.add("comment-thread__timestamp");
if (timestamp.iso) {
timeEl.setAttribute("datetime", timestamp.iso);
}
if (timestamp.title) {
timeEl.title = timestamp.title;
}
timeEl.appendChild(this.document.createTextNode(timestamp.text));
meta.appendChild(timeEl);
}
content.appendChild(meta);
const identityMeta = this.document.createElement("div");
identityMeta.classList.add(
"flex",
"items-center",
"gap-2",
"text-2xs",
"text-muted-strong"
);
identityMeta.hidden = true;
const npubLabel = this.document.createElement("span");
npubLabel.classList.add("truncate");
identityMeta.appendChild(npubLabel);
content.appendChild(identityMeta);
const body = this.document.createElement("p");
body.classList.add("comment-thread__body");
body.appendChild(
this.document.createTextNode(
typeof event.content === "string" ? event.content : ""
)
);
content.appendChild(body);
const actions = this.document.createElement("div");
actions.classList.add("comment-thread__actions");
const copyButton = this.document.createElement("button");
copyButton.type = "button";
copyButton.classList.add("comment-thread__copy-npub");
copyButton.textContent = "Copy npub";
copyButton.dataset.originalLabel = "Copy npub";
copyButton.hidden = true;
this.bindCommentCopyHandler(copyButton);
actions.appendChild(copyButton);
const muteButton = this.document.createElement("button");
muteButton.type = "button";
muteButton.classList.add("comment-thread__mute");
muteButton.appendChild(this.document.createTextNode("Mute author"));
muteButton.addEventListener("click", (domEvent) => {
const triggerElement = domEvent?.currentTarget || muteButton;
this.dispatch("comment:mute-author", {
commentId,
pubkey:
typeof event.pubkey === "string" && event.pubkey
? event.pubkey
: "",
triggerElement,
});
});
actions.appendChild(muteButton);
content.appendChild(actions);
const replies = this.document.createElement("ul");
replies.classList.add("comment-thread__replies");
replies.setAttribute("data-comment-replies", "");
content.appendChild(replies);
listItem.appendChild(avatarWrapper);
listItem.appendChild(content);
return {
listItem,
replies,
elements: {
avatarImg,
avatarLabel,
authorEl,
npubContainer: identityMeta,
npubLabel,
copyButton,
}
};
}
getCommentTimestampLabel(createdAt) {
const numeric = Number(createdAt);
if (!Number.isFinite(numeric) || numeric <= 0) {
return { text: "", iso: "", title: "" };
}
const date = new Date(numeric * 1000);
if (Number.isNaN(date.getTime())) {
return { text: "", iso: "", title: "" };
}
let text = "";
try {
text = formatTimeAgo(numeric);
} catch (error) {
this.logger.log("[VideoModal] Failed to format comment timestamp", error);
}
if (!text) {
text = formatAbsoluteTimestamp(numeric);
}
return {
text,
iso: date.toISOString(),
title: formatAbsoluteTimestamp(numeric),
};
}
bindCommentCopyHandler(button) {
if (!button) {
return;
}
button.addEventListener("click", async () => {
const npub =
typeof button.dataset.npub === "string" && button.dataset.npub
? button.dataset.npub
: "";
const originalLabel =
typeof button.dataset.originalLabel === "string" &&
button.dataset.originalLabel
? button.dataset.originalLabel
: "Copy npub";
if (!button.dataset.originalLabel) {
button.dataset.originalLabel = originalLabel;
}
if (!npub) {
this.showCommentCopyFeedback(button, "Copy failed", "error");
return;
}
try {
const clipboard = this.window?.navigator?.clipboard;
if (!clipboard || typeof clipboard.writeText !== "function") {
throw new Error("Clipboard unavailable");
}
await clipboard.writeText(npub);
this.showCommentCopyFeedback(button, "Copied!", "copied");
} catch (error) {
this.logger.log("[VideoModal] Failed to copy comment npub", error);
this.showCommentCopyFeedback(button, "Copy failed", "error");
}
});
}
showCommentCopyFeedback(button, message, state = "") {
if (!button) {
return;
}
const originalLabel =
typeof button.dataset.originalLabel === "string" &&
button.dataset.originalLabel
? button.dataset.originalLabel
: "Copy npub";
button.textContent = message;
if (state) {
button.dataset.state = state;
} else if (button.dataset.state) {
delete button.dataset.state;
}
const timeoutId = Number.parseInt(
button.dataset.feedbackTimeoutId || "",
10
);
if (
Number.isFinite(timeoutId) &&
this.window &&
typeof this.window.clearTimeout === "function"
) {
this.window.clearTimeout(timeoutId);
}
if (!this.window || typeof this.window.setTimeout !== "function") {
button.textContent = button.dataset.originalLabel || originalLabel;
if (button.dataset.feedbackTimeoutId) {
delete button.dataset.feedbackTimeoutId;
}
return;
}
const nextId = this.window.setTimeout(() => {
button.textContent = button.dataset.originalLabel || originalLabel;
if (button.dataset.state) {
delete button.dataset.state;
}
if (button.dataset.feedbackTimeoutId) {
delete button.dataset.feedbackTimeoutId;
}
}, UI_FEEDBACK_DELAY_MS);
button.dataset.feedbackTimeoutId = String(nextId);
}
updateCommentNodeProfile(refs, profile, cache, failures) {
if (!refs) {
return;
}
if (refs.authorEl) {
refs.authorEl.textContent = profile.displayName;
}
if (refs.avatarLabel) {
refs.avatarLabel.textContent = profile.initial;
refs.avatarLabel.hidden = Boolean(profile.avatarUrl);
}
if (refs.avatarImg) {
if (!refs.avatarImg.dataset.commentAvatarBound) {
const labelRef = refs.avatarLabel;
const imgRef = refs.avatarImg;
refs.avatarImg.addEventListener("error", () => {
const failedSource =
typeof imgRef?.dataset?.commentAvatarSource === "string"
? imgRef.dataset.commentAvatarSource
: "";
if (failedSource) {
registerCommentAvatarFailureUtil({
cache,
failures,
defaultAvatar: this.DEFAULT_PROFILE_AVATAR,
sourceUrl: failedSource,
});
}
if (
imgRef &&
imgRef.dataset.commentAvatarCurrent !== this.DEFAULT_PROFILE_AVATAR
) {
imgRef.dataset.commentAvatarCurrent = this.DEFAULT_PROFILE_AVATAR;
imgRef.src = this.DEFAULT_PROFILE_AVATAR;
return;
}
if (imgRef) {
imgRef.hidden = true;
}
if (labelRef) {
labelRef.hidden = false;
}
});
refs.avatarImg.dataset.commentAvatarBound = "true";
}
const nextSource = profile.avatarSource || "";
refs.avatarImg.dataset.commentAvatarSource = nextSource;
if (profile.avatarUrl) {
if (
refs.avatarImg.dataset.commentAvatarCurrent !== profile.avatarUrl
) {
refs.avatarImg.dataset.commentAvatarCurrent = profile.avatarUrl;
refs.avatarImg.src = profile.avatarUrl;
}
refs.avatarImg.alt = `${profile.displayName}'s avatar`;
refs.avatarImg.hidden = false;
if (refs.avatarLabel) {
refs.avatarLabel.hidden = true;
}
} else {
if (
refs.avatarImg.dataset.commentAvatarCurrent !==
this.DEFAULT_PROFILE_AVATAR
) {
refs.avatarImg.dataset.commentAvatarCurrent =
this.DEFAULT_PROFILE_AVATAR;
refs.avatarImg.src = this.DEFAULT_PROFILE_AVATAR;
}
refs.avatarImg.hidden = true;
if (refs.avatarLabel) {
refs.avatarLabel.hidden = false;
}
}
}
if (refs.npubContainer) {
if (profile.shortNpub) {
refs.npubContainer.hidden = false;
if (refs.npubLabel) {
refs.npubLabel.textContent = profile.shortNpub;
if (profile.npub) {
refs.npubLabel.title = profile.npub;
} else if (refs.npubLabel.title) {
refs.npubLabel.removeAttribute("title");
}
}
} else {
refs.npubContainer.hidden = true;
if (refs.npubLabel) {
refs.npubLabel.textContent = "";
if (refs.npubLabel.title) {
refs.npubLabel.removeAttribute("title");
}
}
}
}
if (refs.copyButton) {
const originalLabel =
typeof refs.copyButton.dataset.originalLabel === "string" &&
refs.copyButton.dataset.originalLabel
? refs.copyButton.dataset.originalLabel
: "Copy npub";
if (profile.npub) {
refs.copyButton.hidden = false;
refs.copyButton.disabled = false;
refs.copyButton.dataset.npub = profile.npub;
refs.copyButton.setAttribute(
"aria-label",
`Copy ${profile.displayName}'s npub`
);
refs.copyButton.textContent = originalLabel;
if (refs.copyButton.dataset.state) {
delete refs.copyButton.dataset.state;
}
} else {
refs.copyButton.hidden = true;
refs.copyButton.disabled = true;
if (refs.copyButton.dataset.state) {
delete refs.copyButton.dataset.state;
}
if (refs.copyButton.dataset.npub) {
delete refs.copyButton.dataset.npub;
}
refs.copyButton.removeAttribute("aria-label");
refs.copyButton.textContent = originalLabel;
}
}
}
}
+489 -109
View File
@@ -1,10 +1,67 @@
import logger from "../../../utils/logger.js";
import { UI_FEEDBACK_DELAY_MS } from "../../../constants.js";
import { CommentsListRenderer } from "./commentsListRenderer.js";
export class CommentsController {
constructor({ modal } = {}) {
this.modal = modal;
this.root = null;
this.playerModal = null;
this.renderer = new CommentsListRenderer({ controller: this });
this.commentsRoot = null;
this.commentsContainer = null;
// Composer elements
this.commentsComposer = null;
this.commentsInput = null;
this.commentsSubmitButton = null;
this.commentsStatusMessage = null;
this.commentsCharCount = null;
this.commentComposerHint = null;
this.commentRetryButton = null;
this.commentsDisabledPlaceholder = null;
this.commentsDisabledPlaceholderDefaultText = "";
// Composer state
this.commentComposerDefaultHint = "";
this.commentComposerDefaultCountText = "";
this.commentStatusDefaultText = "";
this.commentComposerMaxLength = 0;
this.commentComposerState = {
disabled: true,
reason: "",
parentCommentId: null,
};
this.commentCallbacks = {
teardown: null,
};
this.boundCommentSubmitHandler =
this.handleCommentComposerSubmit.bind(this);
this.boundCommentInputHandler =
this.handleCommentComposerInput.bind(this);
this.boundCommentRetryHandler = this.handleCommentRetry.bind(this);
}
get document() {
return this.modal?.document || this.playerModal?.ownerDocument || (typeof document !== 'undefined' ? document : null);
}
get window() {
return this.modal?.window || this.document?.defaultView || (typeof window !== 'undefined' ? window : null);
}
get logger() {
return this.modal?.logger || logger;
}
get commentsList() {
return this.renderer?.listRoot;
}
dispatch(type, detail, options) {
return this.modal?.dispatch(type, detail, options);
}
initialize({ playerModal } = {}) {
@@ -20,15 +77,14 @@ export class CommentsController {
this.ensureCommentsDisabledPlaceholder({ root }) ||
null;
this.root = root;
this.modal.commentsRoot = root;
this.modal.commentsContainer = root;
this.modal.commentsDisabledPlaceholder = commentsDisabledPlaceholder;
this.commentsRoot = root;
this.commentsContainer = root;
this.commentsDisabledPlaceholder = commentsDisabledPlaceholder;
if (commentsDisabledPlaceholder) {
const disabledText = commentsDisabledPlaceholder.textContent || "";
if (disabledText) {
this.modal.commentsDisabledPlaceholderDefaultText = disabledText;
this.commentsDisabledPlaceholderDefaultText = disabledText;
}
}
@@ -38,52 +94,57 @@ export class CommentsController {
);
this.disableComments({
reason: "missing-root",
message: this.modal.commentsDisabledPlaceholderDefaultText ||
message: this.commentsDisabledPlaceholderDefaultText ||
"Comments are unavailable right now.",
});
return;
}
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.commentsComposer =
// Initialize Renderer
const commentsList = root?.querySelector("[data-comments-list]") || null;
const loadMoreButton = root?.querySelector("[data-comments-load-more]") || null;
const countLabel = root?.querySelector("[data-comments-count]") || null;
const emptyState = root?.querySelector("[data-comments-empty]") || null;
this.renderer.initialize({
list: commentsList,
loadMoreButton,
countLabel,
emptyState
});
this.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]") ||
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]") ||
root?.querySelector("[data-comments-status]") ||
null;
this.modal.commentsCharCount =
this.modal.commentsComposer?.querySelector("[data-comments-char-count]") ||
this.commentsCharCount =
this.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 ||
this.commentComposerHint =
this.commentsComposer?.querySelector("#commentComposerHelp") ||
this.commentsComposer?.querySelector(".comment-composer__hint") ||
this.commentComposerHint ||
null;
const missingSelectors = [
{ selector: "[data-comments-list]", element: this.modal.commentsList },
{ selector: "[data-comments-list]", element: commentsList },
{
selector: "[data-comments-composer]",
element: this.modal.commentsComposer,
element: this.commentsComposer,
},
{
selector: "[data-comments-input]",
element: this.modal.commentsInput,
element: this.commentsInput,
},
{
selector: "[data-comments-submit]",
element: this.modal.commentsSubmitButton,
element: this.commentsSubmitButton,
},
]
.filter(({ element }) => !element)
@@ -95,7 +156,7 @@ export class CommentsController {
);
this.disableComments({
reason: "missing-selectors",
message: this.modal.commentsDisabledPlaceholderDefaultText ||
message: this.commentsDisabledPlaceholderDefaultText ||
"Comments are unavailable right now.",
});
return;
@@ -106,56 +167,384 @@ export class CommentsController {
this.playerModal.classList?.remove("comments-disabled");
}
if (this.modal.commentComposerHint) {
const hintText = this.modal.commentComposerHint.textContent || "";
if (this.commentComposerHint) {
const hintText = this.commentComposerHint.textContent || "";
if (hintText) {
this.modal.commentComposerDefaultHint = hintText;
this.commentComposerDefaultHint = hintText;
}
}
if (this.modal.commentsCharCount) {
const countText = this.modal.commentsCharCount.textContent || "";
if (this.commentsCharCount) {
const countText = this.commentsCharCount.textContent || "";
if (countText) {
this.modal.commentComposerDefaultCountText = countText;
this.commentComposerDefaultCountText = countText;
}
}
if (this.modal.commentsStatusMessage) {
const statusText = this.modal.commentsStatusMessage.textContent || "";
if (this.commentsStatusMessage) {
const statusText = this.commentsStatusMessage.textContent || "";
if (statusText) {
this.modal.commentStatusDefaultText = statusText;
this.commentStatusDefaultText = statusText;
}
}
if (this.modal.commentsInput) {
const parsedMax = Number(this.modal.commentsInput.maxLength);
if (this.commentsInput) {
const parsedMax = Number(this.commentsInput.maxLength);
if (Number.isFinite(parsedMax) && parsedMax > 0) {
this.modal.commentComposerMaxLength = parsedMax;
this.commentComposerMaxLength = parsedMax;
}
}
if (this.modal.commentsDisabledPlaceholder) {
this.modal.commentsDisabledPlaceholder.setAttribute("hidden", "");
if (this.commentsDisabledPlaceholder) {
this.commentsDisabledPlaceholder.setAttribute("hidden", "");
}
if (this.modal.commentsComposer) {
this.modal.commentsComposer.removeAttribute("hidden");
if (this.commentsComposer) {
this.commentsComposer.removeAttribute("hidden");
}
this.modal.clearComments();
this.modal.resetCommentComposer();
this.modal.attachCommentEventHandlers();
this.clearComments();
this.resetCommentComposer();
this.attachCommentEventHandlers();
}
attachCommentEventHandlers() {
if (this.commentsComposer && this.boundCommentSubmitHandler) {
this.commentsComposer.addEventListener(
"submit",
this.boundCommentSubmitHandler
);
}
if (this.commentsInput && this.boundCommentInputHandler) {
this.commentsInput.addEventListener(
"input",
this.boundCommentInputHandler
);
}
if (this.commentRetryButton && this.boundCommentRetryHandler) {
this.commentRetryButton.addEventListener(
"click",
this.boundCommentRetryHandler
);
}
}
detachCommentEventHandlers() {
if (this.commentsComposer && this.boundCommentSubmitHandler) {
this.commentsComposer.removeEventListener(
"submit",
this.boundCommentSubmitHandler
);
}
if (this.commentsInput && this.boundCommentInputHandler) {
this.commentsInput.removeEventListener(
"input",
this.boundCommentInputHandler
);
}
if (this.commentRetryButton && this.boundCommentRetryHandler) {
this.commentRetryButton.removeEventListener(
"click",
this.boundCommentRetryHandler
);
}
}
handleCommentComposerSubmit(event) {
if (event && typeof event.preventDefault === "function") {
event.preventDefault();
}
const triggerElement =
(event && event.submitter) || this.commentsSubmitButton || null;
if (this.commentComposerState.reason === "login-required") {
this.modal.dispatch("comment:login-required", { triggerElement });
return;
}
if (this.commentComposerState.disabled) {
return;
}
const text = this.getCommentInputValue();
if (!text) {
return;
}
this.modal.dispatch("comment:submit", {
text,
parentId: this.commentComposerState.parentCommentId || null,
triggerElement,
});
}
handleCommentComposerInput() {
this.updateCommentCharCount();
this.updateCommentSubmitState();
}
handleCommentRetry(event) {
if (event && typeof event.preventDefault === "function") {
event.preventDefault();
}
const triggerElement =
(event && event.currentTarget) || this.commentRetryButton || null;
this.modal.dispatch("comment:retry", {
text: this.getCommentInputValue(),
parentId: this.commentComposerState.parentCommentId || null,
triggerElement,
});
}
getCommentInputValue({ trimmed = true } = {}) {
const value = this.commentsInput ? this.commentsInput.value || "" : "";
return trimmed ? value.trim() : value;
}
updateCommentSubmitState() {
if (!this.commentsSubmitButton) {
return;
}
const hasText = !!this.getCommentInputValue();
const shouldDisable = this.commentComposerState.disabled || !hasText;
this.commentsSubmitButton.disabled = shouldDisable;
}
updateCommentCharCount() {
if (!this.commentsCharCount) {
return;
}
const length = this.commentsInput ? this.commentsInput.value.length : 0;
const safeLength = Number.isFinite(length) ? Math.max(0, length) : 0;
const max = Number.isFinite(this.commentComposerMaxLength)
? Math.max(0, this.commentComposerMaxLength)
: 0;
const label = max > 0 ? `${safeLength} / ${max}` : `${safeLength}`;
this.commentsCharCount.textContent = label;
}
setCommentHintText(text) {
if (!this.commentComposerHint) {
return;
}
const message =
typeof text === "string" && text
? text
: this.commentComposerDefaultHint || "";
this.commentComposerHint.textContent = message;
}
removeCommentRetryButton() {
if (!this.commentRetryButton) {
return;
}
this.commentRetryButton.removeEventListener(
"click",
this.boundCommentRetryHandler
);
if (this.commentRetryButton.parentNode) {
this.commentRetryButton.parentNode.removeChild(this.commentRetryButton);
}
this.commentRetryButton = null;
}
setCommentStatus(message, { showRetry = false } = {}) {
if (!this.commentsStatusMessage) {
return;
}
this.removeCommentRetryButton();
while (this.commentsStatusMessage.firstChild) {
this.commentsStatusMessage.removeChild(
this.commentsStatusMessage.firstChild
);
}
const text =
typeof message === "string" && message
? message
: this.commentStatusDefaultText || "";
if (text) {
this.commentsStatusMessage.appendChild(
this.document.createTextNode(text)
);
}
if (showRetry) {
const spacer = this.document.createTextNode(" ");
const retryButton = this.document.createElement("button");
retryButton.type = "button";
retryButton.classList.add("comment-thread__retry", "focus-ring");
retryButton.appendChild(this.document.createTextNode("Retry"));
retryButton.addEventListener("click", this.boundCommentRetryHandler);
this.commentsStatusMessage.appendChild(spacer);
this.commentsStatusMessage.appendChild(retryButton);
this.commentRetryButton = retryButton;
}
}
setCommentsVisibility(isVisible) {
const root = this.commentsContainer || this.commentsRoot;
if (!root) {
return;
}
const shouldShow = Boolean(isVisible);
if (shouldShow) {
root.removeAttribute("hidden");
root.classList?.remove("hidden");
} else {
root.setAttribute("hidden", "");
root.classList?.add("hidden");
}
}
showCommentsDisabledMessage(message) {
const placeholder = this.commentsDisabledPlaceholder;
if (placeholder) {
const fallbackText = this.commentsDisabledPlaceholderDefaultText || "";
const nextText =
typeof message === "string" && message.trim()
? message.trim()
: fallbackText;
if (nextText) {
placeholder.textContent = nextText;
} else if (fallbackText) {
placeholder.textContent = fallbackText;
}
placeholder.removeAttribute("hidden");
placeholder.classList?.remove("hidden");
}
this.setCommentsVisibility(false);
}
hideCommentsDisabledMessage() {
const placeholder = this.commentsDisabledPlaceholder;
if (placeholder) {
if (this.commentsDisabledPlaceholderDefaultText) {
placeholder.textContent = this.commentsDisabledPlaceholderDefaultText;
}
placeholder.setAttribute("hidden", "");
placeholder.classList?.add("hidden");
}
}
setCommentComposerState({ disabled = false, reason = "" } = {}) {
const normalizedReason = typeof reason === "string" ? reason : "";
let shouldDisable = !!disabled;
switch (normalizedReason) {
case "login-required":
case "submitting":
case "disabled":
shouldDisable = true;
break;
case "error":
shouldDisable = false;
break;
default:
break;
}
this.commentComposerState.disabled = shouldDisable;
this.commentComposerState.reason = normalizedReason;
if (this.commentsInput) {
this.commentsInput.disabled = shouldDisable;
}
if (normalizedReason !== "disabled") {
this.hideCommentsDisabledMessage();
}
if (this.commentsComposer) {
if (normalizedReason === "disabled") {
this.commentsComposer.setAttribute("hidden", "");
} else {
this.commentsComposer.removeAttribute("hidden");
}
}
if (normalizedReason === "login-required") {
this.setCommentHintText("Log in to add a comment.");
this.setCommentStatus("");
this.setCommentsVisibility(true);
} else if (normalizedReason === "submitting") {
this.setCommentHintText(this.commentComposerDefaultHint);
this.setCommentStatus("Posting comment…");
} else if (normalizedReason === "error") {
this.setCommentHintText(this.commentComposerDefaultHint);
this.setCommentStatus("We couldn't post your comment. Try again?", {
showRetry: true,
});
} else if (normalizedReason === "disabled") {
this.setCommentHintText(this.commentComposerDefaultHint);
this.setCommentStatus("");
} else {
this.setCommentHintText(this.commentComposerDefaultHint);
this.setCommentStatus("");
}
this.updateCommentSubmitState();
}
resetCommentComposer() {
if (this.commentsInput) {
this.commentsInput.disabled = false;
this.commentsInput.value = "";
}
this.hideCommentsDisabledMessage();
this.commentComposerState = {
disabled: false,
reason: "",
parentCommentId: null,
};
this.setCommentHintText(this.commentComposerDefaultHint);
this.setCommentStatus("");
this.updateCommentCharCount();
this.updateCommentSubmitState();
}
setCommentSectionCallbacks({ teardown = null } = {}) {
this.commentCallbacks.teardown =
typeof teardown === "function" ? teardown : null;
}
invokeCommentTeardown() {
const teardown = this.commentCallbacks.teardown;
if (typeof teardown !== "function") {
return;
}
this.commentCallbacks.teardown = null;
try {
teardown();
} catch (error) {
this.logger.log("[VideoModal] Failed to teardown comment services", error);
}
}
renderComments(snapshot) {
this.renderer.render(snapshot);
}
appendComment(event) {
this.renderer.append(event);
}
clearComments() {
this.renderer.clear();
}
disableComments({ message = "", reason = "" } = {}) {
const placeholder =
this.modal.commentsDisabledPlaceholder ||
this.ensureCommentsDisabledPlaceholder({ root: this.root }) ||
this.commentsDisabledPlaceholder ||
this.ensureCommentsDisabledPlaceholder({ root: this.commentsRoot }) ||
null;
if (!this.modal.commentsDisabledPlaceholder && placeholder) {
this.modal.commentsDisabledPlaceholder = placeholder;
if (!this.commentsDisabledPlaceholder && placeholder) {
this.commentsDisabledPlaceholder = placeholder;
if (placeholder.textContent) {
this.modal.commentsDisabledPlaceholderDefaultText = placeholder.textContent;
this.commentsDisabledPlaceholderDefaultText = placeholder.textContent;
}
}
@@ -164,23 +553,27 @@ export class CommentsController {
this.playerModal.classList?.add("comments-disabled");
}
if (this.modal.commentsList) {
this.modal.commentsList.setAttribute("hidden", "");
this.modal.commentsList.classList?.add("hidden");
// Hide list via renderer?
// The renderer manages list content, but visibility of the container might be controller concern?
// In old code: this.commentsList.setAttribute("hidden", "");
// Renderer has listRoot.
if (this.renderer.listRoot) {
this.renderer.listRoot.setAttribute("hidden", "");
this.renderer.listRoot.classList?.add("hidden");
}
if (this.modal.commentsComposer) {
this.modal.commentsComposer.setAttribute("hidden", "");
this.modal.commentsComposer.classList?.add("hidden");
if (this.commentsComposer) {
this.commentsComposer.setAttribute("hidden", "");
this.commentsComposer.classList?.add("hidden");
}
const nextMessage =
typeof message === "string" && message.trim()
? message.trim()
: this.modal.commentsDisabledPlaceholderDefaultText ||
: this.commentsDisabledPlaceholderDefaultText ||
"Comments are unavailable right now.";
this.modal.showCommentsDisabledMessage(nextMessage);
this.showCommentsDisabledMessage(nextMessage);
logger.dev?.info?.(
"[VideoModal:comments] Disabled comments handling",
{ reason: reason || "unknown" }
@@ -188,80 +581,68 @@ export class CommentsController {
}
update(action = {}) {
if (!this.modal) {
return;
}
const { type } = action;
switch (type) {
case "set-visibility":
this.modal.setCommentsVisibility(action.visible);
this.setCommentsVisibility(action.visible);
break;
case "render":
this.modal.renderComments(action.snapshot);
this.renderComments(action.snapshot);
break;
case "set-composer-state":
this.modal.setCommentComposerState(action.state);
this.setCommentComposerState(action.state);
break;
case "set-thread-context":
this.modal.setCommentThreadContext(action.context);
this.setCommentThreadContext(action.context);
break;
case "clear":
this.modal.clearComments();
this.clearComments();
break;
default:
break;
}
}
destroy() {
if (!this.modal) {
return;
setCommentThreadContext(context) {
if (context) {
this.commentThreadContext = { ...context };
}
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;
}
destroy() {
this.detachCommentEventHandlers();
this.removeCommentRetryButton();
this.invokeCommentTeardown();
this.renderer.destroy();
this.commentsRoot = null;
this.commentsContainer = null;
this.commentsDisabledPlaceholder = null;
this.commentsComposer = null;
this.commentsInput = null;
this.commentsSubmitButton = null;
this.commentsStatusMessage = null;
this.commentsCharCount = null;
this.commentComposerHint = null;
if (this.playerModal) {
this.playerModal.removeAttribute("data-comments-disabled");
this.playerModal.classList?.remove("comments-disabled");
}
this.root = null;
}
getDocument() {
if (this.modal?.document) {
return this.modal.document;
}
if (this.playerModal?.ownerDocument) {
return this.playerModal.ownerDocument;
}
if (typeof document !== "undefined") {
return document;
}
return null;
this.playerModal = null;
}
ensureCommentsDisabledPlaceholder({ root } = {}) {
const existing =
this.modal?.commentsDisabledPlaceholder ||
this.commentsDisabledPlaceholder ||
root?.querySelector?.("[data-comments-disabled-placeholder]") ||
this.playerModal?.querySelector?.("[data-comments-disabled-placeholder]");
if (existing) {
return existing;
}
const doc = this.getDocument();
const doc = this.document;
const container = root?.parentElement || this.playerModal || null;
if (!doc || !container) {
return null;
@@ -275,7 +656,7 @@ export class CommentsController {
placeholder.classList.add("comment-thread__disabled");
const defaultText =
this.modal?.commentsDisabledPlaceholderDefaultText ||
this.commentsDisabledPlaceholderDefaultText ||
"Comments are unavailable right now.";
placeholder.textContent = defaultText;
@@ -288,4 +669,3 @@ export class CommentsController {
return placeholder;
}
}
@@ -0,0 +1,652 @@
import {
normalizeCommentAvatarKey as normalizeCommentAvatarKeyUtil,
resolveCommentAvatarAsset as resolveCommentAvatarAssetUtil,
} from "./utils/commentAvatar.js";
import { HEX64_REGEX } from "../../../utils/hex.js";
import { sanitizeProfileMediaUrl } from "../../../utils/profileMedia.js";
import logger from "../../../utils/logger.js";
import { CommentNodeFactory } from "./commentNodeFactory.js";
export class CommentsListRenderer {
constructor({ controller } = {}) {
this.controller = controller;
this.document = controller.document;
this.window = controller.window;
this.logger = controller.logger || logger;
this.helpers = controller.modal?.helpers || {}; // Need helpers for npub format
this.listRoot = null;
this.loadMoreButton = null;
this.countLabel = null;
this.emptyState = null;
this.commentProfiles = new Map();
this.commentNodes = new Map();
this.commentChildLists = new Map();
this.commentNodeElements = new Map();
this.commentNpubEncodingFailures = new Set();
this.commentAvatarCache = new Map();
this.commentAvatarFailures = new Set();
this.commentThreadContext = {
videoEventId: "",
parentCommentId: null,
};
this.DEFAULT_PROFILE_AVATAR = "assets/svg/default-profile.svg";
this.boundCommentLoadMoreHandler = this.handleCommentLoadMore.bind(this);
this.nodeFactory = new CommentNodeFactory({
document: this.document,
window: this.window,
logger: this.logger,
dispatch: (type, detail) => this.controller.dispatch(type, detail),
DEFAULT_PROFILE_AVATAR: this.DEFAULT_PROFILE_AVATAR
});
}
initialize({ list, loadMoreButton, countLabel, emptyState }) {
this.listRoot = list;
this.loadMoreButton = loadMoreButton;
this.countLabel = countLabel;
this.emptyState = emptyState;
if (this.loadMoreButton) {
this.loadMoreButton.addEventListener("click", this.boundCommentLoadMoreHandler);
}
}
destroy() {
if (this.loadMoreButton) {
this.loadMoreButton.removeEventListener("click", this.boundCommentLoadMoreHandler);
}
this.listRoot = null;
this.loadMoreButton = null;
this.countLabel = null;
this.emptyState = null;
this.commentProfiles.clear();
this.commentNodes.clear();
this.commentChildLists.clear();
this.commentNodeElements.clear();
this.commentAvatarCache.clear();
this.commentAvatarFailures.clear();
}
handleCommentLoadMore(event) {
if (event && typeof event.preventDefault === "function") {
event.preventDefault();
}
const triggerElement =
(event && event.currentTarget) || this.loadMoreButton || null;
this.controller.dispatch("comment:load-more", {
parentId: this.commentThreadContext.parentCommentId || null,
triggerElement,
});
}
render(snapshot) {
if (!this.listRoot || !this.document) {
return;
}
const commentsMap = this.normalizeCommentMap(snapshot?.commentsById);
const childrenMap = this.normalizeChildrenMap(snapshot?.childrenByParent);
this.commentProfiles = this.normalizeProfileMap(snapshot?.profiles);
this.commentThreadContext.videoEventId =
typeof snapshot?.videoEventId === "string"
? snapshot.videoEventId.trim()
: "";
this.commentThreadContext.parentCommentId =
typeof snapshot?.parentCommentId === "string" &&
snapshot.parentCommentId.trim()
? snapshot.parentCommentId.trim()
: null;
this.commentNodes.clear();
this.commentChildLists = new Map();
this.commentChildLists.set(null, this.listRoot);
this.listRoot.textContent = "";
const fragment = this.document.createDocumentFragment();
const topLevelIds = this.getChildCommentIds(childrenMap, null);
topLevelIds.forEach((commentId) => {
const node = this.buildCommentTree(
commentId,
commentsMap,
childrenMap,
0
);
if (node) {
fragment.appendChild(node);
}
});
this.listRoot.appendChild(fragment);
this.updateCommentCount(this.commentNodes.size);
this.toggleCommentEmptyState(this.commentNodes.size === 0);
}
clear() {
this.commentNodes.clear();
this.commentChildLists.clear();
this.commentNodeElements.clear();
this.commentNpubEncodingFailures.clear();
if (this.listRoot) {
this.listRoot.textContent = "";
this.commentChildLists.set(null, this.listRoot);
}
this.updateCommentCount(0);
this.toggleCommentEmptyState(true);
}
append(event) {
if (!event || typeof event !== "object" || !this.listRoot) {
return;
}
const commentId =
typeof event.id === "string" && event.id.trim() ? event.id.trim() : "";
if (!commentId || this.commentNodes.has(commentId)) {
return;
}
if (event.profile && typeof event.pubkey === "string") {
this.mergeCommentProfile(event.pubkey, event.profile);
}
const parentId = this.extractParentCommentId(event);
const parentNode =
parentId && this.commentNodes.has(parentId)
? this.commentNodes.get(parentId)
: null;
const depth = parentNode
? Math.max(0, Number(parentNode.dataset?.commentDepth) || 0) + 1
: 0;
const node = this.createCommentNode(event, { depth });
if (!node) {
return;
}
const container = this.getCommentChildrenContainer(parentId);
if (!container) {
this.logger.log("[VideoModal] Missing container for appended comment", parentId);
return;
}
this.insertCommentNodeSorted(container, node);
this.updateCommentCount(this.commentNodes.size);
this.toggleCommentEmptyState(this.commentNodes.size === 0);
}
insertCommentNodeSorted(container, node) {
if (!container || !node) {
return;
}
const timestamp = Number(node.dataset.timestamp || 0);
let inserted = false;
const children = container.children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
const childTimestamp = Number(child.dataset.timestamp || 0);
if (childTimestamp > timestamp) {
container.insertBefore(node, child);
inserted = true;
break;
}
}
if (!inserted) {
container.appendChild(node);
}
}
buildCommentTree(commentId, commentsMap, childrenMap, depth = 0) {
const normalizedId =
typeof commentId === "string" && commentId.trim()
? commentId.trim()
: "";
if (!normalizedId) {
return null;
}
const event = commentsMap.get(normalizedId);
if (!event) {
return null;
}
const node = this.createCommentNode(event, { depth });
if (!node) {
return null;
}
const repliesContainer = this.commentChildLists.get(normalizedId);
if (repliesContainer) {
const childIds = this.getChildCommentIds(childrenMap, normalizedId);
if (childIds.length) {
const fragment = this.document.createDocumentFragment();
childIds.forEach((childId) => {
const childNode = this.buildCommentTree(
childId,
commentsMap,
childrenMap,
depth + 1
);
if (childNode) {
fragment.appendChild(childNode);
}
});
if (fragment.childNodes.length) {
repliesContainer.appendChild(fragment);
}
}
}
return node;
}
createCommentNode(event, { depth = 0 } = {}) {
if (!event || typeof event !== "object") {
return null;
}
const commentId =
typeof event.id === "string" && event.id.trim() ? event.id.trim() : "";
if (!commentId) {
return null;
}
if (this.commentNodes.has(commentId)) {
return this.commentNodes.get(commentId);
}
const { listItem, replies, elements } = this.nodeFactory.createCommentNode(event, { depth });
this.commentNodes.set(commentId, listItem);
this.commentChildLists.set(commentId, replies);
this.commentNodeElements.set(commentId, elements);
this.updateCommentNodeProfile(commentId, event.pubkey);
return listItem;
}
updateCommentNodeProfile(commentId, pubkey) {
if (!(this.commentNodeElements instanceof Map)) {
this.commentNodeElements = new Map();
}
if (!commentId || !this.commentNodeElements.has(commentId)) {
return;
}
const refs = this.commentNodeElements.get(commentId);
const profile = this.getCommentAuthorProfile(pubkey);
this.nodeFactory.updateCommentNodeProfile(
refs,
profile,
this.commentAvatarCache,
this.commentAvatarFailures
);
}
updateCommentNodesForAuthor(pubkey) {
if (typeof pubkey !== "string" || !pubkey.trim()) {
return;
}
const normalized = pubkey.trim().toLowerCase();
this.commentNodes.forEach((node, id) => {
const author =
typeof node?.dataset?.commentAuthor === "string"
? node.dataset.commentAuthor.trim().toLowerCase()
: "";
if (author && author === normalized) {
this.updateCommentNodeProfile(id, pubkey);
}
});
}
getCommentAuthorName(pubkey) {
return this.getCommentAuthorProfile(pubkey).displayName;
}
getCommentAvatarInitial(pubkey) {
return this.getCommentAuthorProfile(pubkey).initial;
}
getCommentAuthorProfile(pubkey) {
const normalized =
typeof pubkey === "string" && pubkey.trim() ? pubkey.trim() : "";
const profileEntry = normalized
? this.commentProfiles.get(normalized.toLowerCase()) || null
: null;
const rawPicture =
profileEntry && typeof profileEntry.picture === "string"
? profileEntry.picture
: "";
const sanitizedPicture = sanitizeProfileMediaUrl(rawPicture);
const avatar = this.resolveCommentAvatarAsset(normalized, sanitizedPicture);
let npub = "";
if (profileEntry && typeof profileEntry.npub === "string") {
const trimmed = profileEntry.npub.trim();
if (trimmed) {
if (trimmed.startsWith("npub")) {
npub = trimmed;
} else if (HEX64_REGEX.test(trimmed)) {
npub = this.encodeCommentPubkeyToNpub(trimmed);
}
}
}
if (!npub && profileEntry && typeof profileEntry.pubkey === "string") {
const trimmed = profileEntry.pubkey.trim();
if (trimmed) {
npub = this.encodeCommentPubkeyToNpub(trimmed);
}
}
if (!npub && normalized) {
npub = this.encodeCommentPubkeyToNpub(normalized);
}
let displayName = "";
if (profileEntry && typeof profileEntry === "object") {
const candidates = [
profileEntry.display_name,
profileEntry.name,
profileEntry.username,
];
for (const candidate of candidates) {
if (typeof candidate !== "string") {
continue;
}
const trimmed = candidate.trim();
if (trimmed) {
displayName = trimmed;
break;
}
}
}
if (displayName && HEX64_REGEX.test(displayName)) {
displayName = "";
}
if (!displayName && npub && this.helpers.formatShortNpub) {
displayName = this.helpers.formatShortNpub(npub);
}
if (!displayName) {
displayName = "Anonymous";
}
const shortNpub = npub && this.helpers.formatShortNpub ? this.helpers.formatShortNpub(npub) : "";
const initialCandidate = displayName.trim().charAt(0) || "";
const initial = initialCandidate ? initialCandidate.toUpperCase() : "?";
return {
displayName,
avatarUrl: avatar.url,
avatarSource: avatar.source,
npub,
shortNpub,
initial,
};
}
resolveCommentAvatarAsset(pubkey, sanitizedPicture) {
return resolveCommentAvatarAssetUtil({
cache: this.commentAvatarCache,
failures: this.commentAvatarFailures,
defaultAvatar: this.DEFAULT_PROFILE_AVATAR,
pubkey,
sanitizedPicture,
});
}
normalizeCommentAvatarKey(value) {
return normalizeCommentAvatarKeyUtil(value);
}
encodeCommentPubkeyToNpub(pubkey) {
if (typeof pubkey !== "string") {
return "";
}
const trimmed = pubkey.trim();
if (!trimmed) {
return "";
}
if (trimmed.startsWith("npub1")) {
return trimmed;
}
if (!HEX64_REGEX.test(trimmed)) {
return "";
}
if (this.commentNpubEncodingFailures.has(trimmed)) {
return "";
}
try {
const encoded = this.window?.NostrTools?.nip19?.npubEncode?.(trimmed);
if (typeof encoded === "string" && encoded) {
return encoded;
}
} catch (error) {
if (!this.commentNpubEncodingFailures.has(trimmed)) {
this.logger.log("[VideoModal] Failed to encode comment author pubkey", error);
}
}
this.commentNpubEncodingFailures.add(trimmed);
return "";
}
normalizeCommentMap(candidate) {
if (candidate instanceof Map) {
return new Map(candidate);
}
if (Array.isArray(candidate)) {
const map = new Map();
candidate.forEach(([key, value]) => {
if (typeof key === "string" && key.trim()) {
map.set(key.trim(), value);
}
});
return map;
}
if (candidate && typeof candidate === "object") {
const map = new Map();
Object.entries(candidate).forEach(([key, value]) => {
if (typeof key === "string" && key.trim()) {
map.set(key.trim(), value);
}
});
return map;
}
return new Map();
}
normalizeChildrenMap(candidate) {
const map = new Map();
if (candidate instanceof Map) {
candidate.forEach((value, key) => {
map.set(this.normalizeParentKey(key), this.normalizeIdList(value));
});
return map;
}
if (Array.isArray(candidate)) {
candidate.forEach(([key, value]) => {
map.set(this.normalizeParentKey(key), this.normalizeIdList(value));
});
return map;
}
if (candidate && typeof candidate === "object") {
Object.entries(candidate).forEach(([key, value]) => {
map.set(this.normalizeParentKey(key), this.normalizeIdList(value));
});
}
return map;
}
normalizeProfileMap(candidate) {
const map = new Map();
if (candidate instanceof Map) {
candidate.forEach((value, key) => {
const normalizedKey =
typeof key === "string" && key.trim() ? key.trim().toLowerCase() : "";
if (normalizedKey) {
map.set(normalizedKey, value);
}
});
return map;
}
if (Array.isArray(candidate)) {
candidate.forEach(([key, value]) => {
const normalizedKey =
typeof key === "string" && key.trim() ? key.trim().toLowerCase() : "";
if (normalizedKey) {
map.set(normalizedKey, value);
}
});
return map;
}
if (candidate && typeof candidate === "object") {
Object.entries(candidate).forEach(([key, value]) => {
const normalizedKey =
typeof key === "string" && key.trim() ? key.trim().toLowerCase() : "";
if (normalizedKey) {
map.set(normalizedKey, value);
}
});
}
return map;
}
normalizeIdList(value) {
if (!Array.isArray(value)) {
return [];
}
return value
.map((entry) =>
typeof entry === "string" && entry.trim() ? entry.trim() : ""
)
.filter(Boolean);
}
normalizeParentKey(value) {
if (value === null || value === undefined) {
return null;
}
if (typeof value === "string") {
const trimmed = value.trim();
if (
!trimmed ||
trimmed === "null" ||
trimmed === "undefined" ||
trimmed === "__root__"
) {
return null;
}
return trimmed;
}
return this.normalizeParentKey(String(value));
}
getChildCommentIds(childrenMap, parentId) {
const key = this.normalizeParentKey(parentId);
const list = childrenMap.get(key);
return Array.isArray(list) ? [...list] : [];
}
getCommentChildrenContainer(parentId) {
const key = this.normalizeParentKey(parentId);
if (key === null) {
return this.commentChildLists.get(null) || this.listRoot;
}
if (this.commentChildLists.has(key)) {
return this.commentChildLists.get(key);
}
const parentNode = this.commentNodes.get(key);
if (parentNode) {
const existing = parentNode.querySelector("[data-comment-replies]");
if (existing) {
this.commentChildLists.set(key, existing);
return existing;
}
}
return this.commentChildLists.get(null) || this.listRoot;
}
extractParentCommentId(event) {
const tags = Array.isArray(event?.tags) ? event.tags : [];
const values = [];
for (const tag of tags) {
if (!Array.isArray(tag) || tag.length < 2) {
continue;
}
const [name, value] = tag;
if (name !== "e") {
continue;
}
const normalized = typeof value === "string" ? value.trim() : "";
if (!normalized || normalized === this.commentThreadContext.videoEventId) {
continue;
}
values.push(normalized);
}
if (!values.length) {
return null;
}
return values[values.length - 1];
}
mergeCommentProfile(pubkey, profile) {
if (typeof pubkey !== "string" || !pubkey.trim()) {
return;
}
const normalized = pubkey.trim().toLowerCase();
this.commentProfiles.set(normalized, profile);
this.updateCommentNodesForAuthor(pubkey);
}
updateCommentCount(count) {
if (!this.countLabel) {
return;
}
const numeric = Number.isFinite(count) ? Math.max(0, Math.floor(count)) : 0;
const label = numeric === 1 ? "1 comment" : `${numeric} comments`;
this.countLabel.textContent = label;
}
toggleCommentEmptyState(isEmpty) {
const shouldShow = Boolean(isEmpty);
if (this.emptyState) {
if (shouldShow) {
this.emptyState.removeAttribute("hidden");
} else {
this.emptyState.setAttribute("hidden", "");
}
}
if (this.listRoot) {
if (shouldShow) {
this.listRoot.setAttribute("data-empty", "true");
} else {
this.listRoot.removeAttribute("data-empty");
}
}
}
}