mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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]>
282 lines
8.3 KiB
JavaScript
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);
|
|
}
|
|
}
|