Files
bitvid/js/ui/profileModal/ProfileAdminRenderer.js
T
thePR0M3TH3ANandClaude Opus 4.8 27808533d2 Mobile: same stacked treatment for the Admin + Moderation account cards
Extends the .profile-account-card pattern to the remaining compact account
lists so they all behave consistently on phones:

- Admin lists (whitelist/blacklist/editors): use the shared
  .profile-account-card / __actions classes (was bespoke responsive utilities;
  now full-width equal-width action buttons on mobile like the others).
- Moderation overrides: the summary+actions row (a nested flex inside an
  already-padded card) uses a new padding-less .profile-account-card__row
  variant so it stacks on phones too.

The account switcher (single tap-button with a column meta-stack) and DM
conversation rows (tap-to-open, no action buttons) are already mobile-friendly
and structurally different, so they're intentionally left as-is.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-08 07:35:05 -04:00

282 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 profile-account-card";
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 = "profile-account-card__actions";
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);
}
}