Files
bitvid/js/ui/dm/Avatar.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

73 lines
2.0 KiB
JavaScript

import { applyAdminStar, isAdminActor } from "../adminBadge.js";
const DEFAULT_AVATAR_ALT = "Avatar";
function getInitials(label = "") {
if (typeof label !== "string") {
return "";
}
const words = label.trim().split(/\s+/).filter(Boolean);
if (!words.length) {
return "";
}
const initials = words.slice(0, 2).map((word) => word[0].toUpperCase());
return initials.join("");
}
export function Avatar({
document: doc,
src = "",
alt = DEFAULT_AVATAR_ALT,
size = "md",
initials = "",
status = "",
adminId = "",
} = {}) {
if (!doc) {
throw new Error("Avatar requires a document reference.");
}
const avatar = doc.createElement("div");
// Explicit mapping ensures Tailwind scanner detects these classes
const sizeClass = {
sm: "dm-avatar--sm",
md: "dm-avatar--md",
}[size] || `dm-avatar--${size}`;
avatar.className = `dm-avatar ${sizeClass}`;
if (status) {
avatar.dataset.status = status;
}
const resolvedInitials = initials || getInitials(alt);
if (src) {
const img = doc.createElement("img");
img.className = "dm-avatar__image";
img.alt = alt || DEFAULT_AVATAR_ALT;
img.loading = "lazy";
img.decoding = "async";
img.src = src;
avatar.appendChild(img);
} else {
const fallback = doc.createElement("span");
fallback.className = "dm-avatar__fallback";
fallback.textContent = resolvedInitials || "?";
avatar.appendChild(fallback);
}
// The circular avatar clips its contents, so an admin star can't live inside
// it. Only when the actor is an admin do we wrap the avatar in a non-clipped
// relative container and hang the star off that — non-admin avatars are
// returned exactly as before to keep existing layouts untouched.
if (adminId && isAdminActor(adminId)) {
const wrap = doc.createElement("span");
wrap.className = "dm-avatar-wrap";
wrap.appendChild(avatar);
applyAdminStar(wrap, adminId, { doc });
return wrap;
}
return avatar;
}