Files
bitvid/js/ui/dm/ContactRow.js
T
thePR0M3TH3ANandClaude Opus 4.8 610935ac03 Admin star badge on admin avatars across the app
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]>
2026-07-07 10:10:19 -04:00

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;
}