Files
bitvid/js/ui/profileModal/ProfileAdminRenderer.js
T
google-labs-jules[bot]andPR0M3TH3AN f35d27d471 fix: repair profileModalController decomposition by adding delegate methods
- Added delegate methods for `refreshAdminPaneState`, `refreshModerationSettingsUi`, `updateModerationTrustStats`, `runAdminMutation`, `sendAdminListNotification`, `describeAdminError`, `describeNotificationError`, `notifyAdminError` to `ProfileModalController.js`.
- Added aliases for properties like `moderatorSection` in `ProfileModalController.js` to satisfy tests.
- Removed duplicated method implementations from `ProfileModalController.js`.
- Verified with unit tests (`tests/profile-modal-controller.test.mjs`) and linting.
- Reduced `ProfileModalController.js` size by ~2000 lines.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-14 10:59:14 +00:00

284 lines
8.3 KiB
JavaScript

import { devLogger } from "../../utils/logger.js";
const FALLBACK_PROFILE_AVATAR = "assets/svg/default-profile.svg";
export class ProfileAdminRenderer {
constructor(mainController) {
this.mainController = mainController;
}
storeAdminEmptyMessages(controller) {
const capture = (element) => {
if (element instanceof HTMLElement && !element.dataset.defaultMessage) {
element.dataset.defaultMessage = element.textContent || "";
}
};
capture(controller.moderatorEmpty);
capture(controller.whitelistEmpty);
capture(controller.blacklistEmpty);
}
setAdminLoading(controller, isLoading) {
this.storeAdminEmptyMessages(controller);
if (this.mainController.panes.admin instanceof HTMLElement) {
this.mainController.panes.admin.setAttribute("aria-busy", isLoading ? "true" : "false");
}
const toggleMessage = (element, message) => {
if (!(element instanceof HTMLElement)) {
return;
}
if (isLoading) {
element.textContent = message;
element.classList.remove("hidden");
} else {
element.textContent = element.dataset.defaultMessage || element.textContent;
}
};
toggleMessage(controller.moderatorEmpty, "Loading moderators…");
toggleMessage(controller.whitelistEmpty, "Loading whitelist…");
toggleMessage(controller.blacklistEmpty, "Loading blacklist…");
}
clearAdminLists(controller) {
this.storeAdminEmptyMessages(controller);
if (controller.adminModeratorList) {
controller.adminModeratorList.textContent = "";
}
if (controller.whitelistList) {
controller.whitelistList.textContent = "";
}
if (controller.blacklistList) {
controller.blacklistList.textContent = "";
}
if (controller.moderatorEmpty instanceof HTMLElement) {
controller.moderatorEmpty.textContent =
controller.moderatorEmpty.dataset.defaultMessage ||
controller.moderatorEmpty.textContent;
controller.moderatorEmpty.classList.remove("hidden");
}
if (controller.whitelistEmpty instanceof HTMLElement) {
controller.whitelistEmpty.textContent =
controller.whitelistEmpty.dataset.defaultMessage ||
controller.whitelistEmpty.textContent;
controller.whitelistEmpty.classList.remove("hidden");
}
if (controller.blacklistEmpty instanceof HTMLElement) {
controller.blacklistEmpty.textContent =
controller.blacklistEmpty.dataset.defaultMessage ||
controller.blacklistEmpty.textContent;
controller.blacklistEmpty.classList.remove("hidden");
}
}
normalizeAdminListEntries(entries) {
const collected = [];
const seen = new Set();
const append = (value) => {
if (typeof value !== "string") {
return;
}
const trimmed = value.trim();
if (!trimmed || seen.has(trimmed)) {
return;
}
seen.add(trimmed);
collected.push(trimmed);
};
if (Array.isArray(entries)) {
entries.forEach(append);
} else if (entries && typeof entries?.[Symbol.iterator] === "function") {
for (const entry of entries) {
append(entry);
}
} else if (entries && typeof entries === "object") {
Object.values(entries).forEach(append);
}
try {
collected.sort((a, b) => a.localeCompare(b));
} catch (error) {
devLogger.warn(
"[profileModal] Failed to sort admin list entries, using fallback order.",
error,
);
}
return collected;
}
renderAdminList(listEl, emptyEl, entries, options = {}) {
if (!(listEl instanceof HTMLElement) || !(emptyEl instanceof HTMLElement)) {
return;
}
const {
onRemove,
removeLabel = "Remove",
confirmMessage,
removable = true,
overlapSet,
overlapLabel,
} = options;
const formatNpub =
typeof this.mainController.formatShortNpub === "function"
? (value) => this.mainController.formatShortNpub(value)
: (value) => (typeof value === "string" ? value : "");
const entriesNeedingFetch = new Set();
listEl.textContent = "";
const values = this.normalizeAdminListEntries(entries);
const toggleHiddenState = (element, shouldHide) => {
if (!(element instanceof HTMLElement)) {
return;
}
if (shouldHide) {
element.classList.add("hidden");
element.setAttribute("hidden", "");
} else {
element.classList.remove("hidden");
element.removeAttribute("hidden");
}
};
if (!values.length) {
toggleHiddenState(emptyEl, false);
toggleHiddenState(listEl, true);
return;
}
toggleHiddenState(emptyEl, true);
toggleHiddenState(listEl, false);
values.forEach((npub) => {
const item = document.createElement("li");
item.className =
"card flex flex-col gap-3 p-4 sm:flex-row sm:items-center sm:justify-between";
const normalizedNpub = typeof npub === "string" ? npub.trim() : "";
const comparableNpub =
this.normalizeNpubValue(normalizedNpub) || normalizedNpub;
const decodedHex =
normalizedNpub && normalizedNpub.startsWith("npub1")
? this.mainController.safeDecodeNpub(normalizedNpub)
: null;
const normalizedHex =
decodedHex && /^[0-9a-f]{64}$/i.test(decodedHex)
? decodedHex.toLowerCase()
: null;
let cachedProfile = null;
if (normalizedHex) {
const cacheEntry = this.mainController.services.getProfileCacheEntry(normalizedHex);
cachedProfile = cacheEntry?.profile || null;
if (!cacheEntry) {
entriesNeedingFetch.add(normalizedHex);
}
}
const encodedNpub =
normalizedHex && typeof this.mainController.safeEncodeNpub === "function"
? this.mainController.safeEncodeNpub(normalizedHex)
: normalizedNpub;
const displayNpub = formatNpub(encodedNpub) || encodedNpub || normalizedNpub;
const displayName =
cachedProfile?.name?.trim() || displayNpub || "Unknown profile";
const avatarSrc =
cachedProfile?.picture || FALLBACK_PROFILE_AVATAR;
const summary = this.mainController.dmController.createCompactProfileSummary({
displayName,
displayNpub,
avatarSrc,
});
if (
summary &&
overlapLabel &&
overlapSet instanceof Set &&
comparableNpub &&
overlapSet.has(comparableNpub)
) {
const overlapBadge = document.createElement("span");
overlapBadge.className = "badge whitespace-nowrap";
overlapBadge.dataset.variant = "warning";
overlapBadge.textContent = overlapLabel;
summary.appendChild(overlapBadge);
}
const actions = document.createElement("div");
actions.className =
"flex flex-wrap items-center justify-end gap-2 sm:flex-none";
const viewButton = this.mainController.createViewChannelButton({
targetNpub: encodedNpub,
displayNpub,
});
if (viewButton) {
actions.appendChild(viewButton);
}
const copyButton = this.mainController.createCopyNpubButton({
targetNpub: encodedNpub,
displayNpub,
});
if (copyButton) {
actions.appendChild(copyButton);
}
if (removable && typeof onRemove === "function") {
const removeBtn = this.mainController.createRemoveButton({
label: removeLabel,
confirmMessage,
confirmValue: displayNpub,
onRemove: (button) => onRemove(npub, button),
});
if (removeBtn) {
actions.appendChild(removeBtn);
}
}
item.appendChild(summary);
if (actions.childElementCount > 0) {
item.appendChild(actions);
}
listEl.appendChild(item);
});
if (
entriesNeedingFetch.size &&
typeof this.mainController.services.batchFetchProfiles === "function"
) {
this.mainController.services.batchFetchProfiles(entriesNeedingFetch);
}
}
normalizeNpubValue(value) {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
if (!trimmed) {
return null;
}
if (trimmed.startsWith("npub1")) {
return trimmed;
}
const normalizedHex = this.mainController.normalizeHexPubkey(trimmed);
if (!normalizedHex) {
return null;
}
return this.mainController.safeEncodeNpub(normalizedHex);
}
}