mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Show a small gold "verified admin" star in the corner opposite the red notification dot whenever a rendered profile belongs to a bitvid admin (the super admin or a current list editor). - adminBadge.js (new): isAdminActor(hex|npub) sync check against accessControl, and applyAdminStar(container, id) which adds/removes a single `.admin-star` in a non-clipped, positioned container. Idempotent. - CSS: `.admin-star` (top-left gold disc, mirrors the top-right dot), `.dm-avatar-wrap`, and position:relative on the comment avatar wrapper. - Profile button: app.refreshAdminStar() lights the logged-in user's star; re-runs on login/logout and on accessControl.onEditorsChange (so a late-loading editor list still resolves). - Comments: star on comment-author avatars (commentNodeFactory). - Channel page: wrap the clipped avatar and star the channel owner. - DMs: Avatar gains an opt-in adminId — only admin avatars get wrapped so non-admin rows are byte-identical; ContactRow passes the peer pubkey. - tests: admin-badge unit coverage (recognition, idempotency, removal). Co-Authored-By: Claude Opus 4.8 <[email protected]>
98 lines
2.3 KiB
JavaScript
98 lines
2.3 KiB
JavaScript
import { Avatar } from "./Avatar.js";
|
|
import { formatZapAmount } from "./zapHelpers.js";
|
|
|
|
function createElement(doc, tag, className, text) {
|
|
const element = doc.createElement(tag);
|
|
if (className) {
|
|
element.className = className;
|
|
}
|
|
if (typeof text === "string") {
|
|
element.textContent = text;
|
|
}
|
|
return element;
|
|
}
|
|
|
|
export function ContactRow({
|
|
document: doc,
|
|
contact = {},
|
|
isActive = false,
|
|
tabIndex = -1,
|
|
onSelect,
|
|
} = {}) {
|
|
if (!doc) {
|
|
throw new Error("ContactRow requires a document reference.");
|
|
}
|
|
|
|
const {
|
|
id = "",
|
|
name = "Unknown",
|
|
preview = "",
|
|
timestamp = "",
|
|
unreadCount = 0,
|
|
avatarSrc = "",
|
|
status = "",
|
|
zapTotalSats = null,
|
|
pubkey = "",
|
|
} = contact;
|
|
|
|
const button = createElement(doc, "button", "dm-contact-row");
|
|
button.type = "button";
|
|
button.dataset.conversationId = id;
|
|
button.setAttribute("role", "option");
|
|
button.setAttribute("aria-selected", String(Boolean(isActive)));
|
|
button.tabIndex = tabIndex;
|
|
if (isActive) {
|
|
button.classList.add("dm-contact-row--active");
|
|
}
|
|
|
|
const avatar = Avatar({
|
|
document: doc,
|
|
src: avatarSrc,
|
|
alt: name,
|
|
size: "md",
|
|
initials: name,
|
|
status,
|
|
adminId: pubkey || id,
|
|
});
|
|
button.appendChild(avatar);
|
|
|
|
const content = createElement(doc, "div", "dm-contact-row__content");
|
|
const header = createElement(doc, "div", "dm-contact-row__header");
|
|
const nameEl = createElement(doc, "span", "dm-contact-row__name", name);
|
|
header.appendChild(nameEl);
|
|
|
|
const meta = createElement(doc, "span", "dm-contact-row__meta");
|
|
|
|
if (Number.isFinite(zapTotalSats)) {
|
|
const zapTotal = createElement(
|
|
doc,
|
|
"span",
|
|
"dm-contact-row__zap",
|
|
formatZapAmount(zapTotalSats, { compact: true }),
|
|
);
|
|
zapTotal.setAttribute(
|
|
"aria-label",
|
|
`Total zaps ${formatZapAmount(zapTotalSats)}`,
|
|
);
|
|
meta.appendChild(zapTotal);
|
|
}
|
|
|
|
header.appendChild(meta);
|
|
content.appendChild(header);
|
|
|
|
if (unreadCount > 0) {
|
|
const badge = createElement(doc, "span", "dm-contact-row__badge");
|
|
badge.textContent = `${unreadCount}`;
|
|
badge.setAttribute("aria-label", `${unreadCount} unread messages`);
|
|
content.appendChild(badge);
|
|
}
|
|
|
|
button.appendChild(content);
|
|
|
|
if (typeof onSelect === "function") {
|
|
button.addEventListener("click", () => onSelect(contact));
|
|
}
|
|
|
|
return button;
|
|
}
|