Admin ring on comments/channel + pending-submissions dots on Admin tab & Submissions subtab

- adminBadge.js: add applyAdminRing(el, id) for surfaces that already place the
  star on a specific element and just need the ring.
- comments: ring + star on admin authors' avatars; round the comment avatar box
  so the ring reads as a circle.
- channel page: ring the inner rounded avatar box (star already on the wrap).
- Notification dots: red dot on the profile modal's Admin nav tab and on the
  Submissions subtab button when there are pending submissions, so admins know
  where to look. profileModalController.setSubmissionsUnreadIndicator toggles
  both (Admin nav dot gated on the tab being visible); driven from
  applyProfileNotificationDot off app._submissionsPending.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-07 14:59:09 -04:00
1 parent 37edbd18d2
commit d091ce8bac
7 files changed
+100 -7

No files matched your search

+11 -1
View File
@@ -423,6 +423,11 @@
/>
</svg>
Admin (Moderation)
<span
id="profileNavAdminUnreadDot"
class="profile-nav__unread-dot"
aria-hidden="true"
></span>
</button>
<button
id="profileMobileLockNowBtn"
@@ -1884,9 +1889,14 @@
data-admin-subtab="submissions"
aria-controls="adminSubmissionsSection"
aria-selected="false"
class="btn-ghost focus-ring px-3 py-1.5 text-sm font-medium hidden"
class="admin-subtab-has-dot btn-ghost focus-ring px-3 py-1.5 text-sm font-medium hidden"
>
Submissions
<span
id="adminSubtabSubmissionsDot"
class="admin-subtab-dot"
aria-hidden="true"
></span>
</button>
</div>
+26
View File
@@ -2764,6 +2764,8 @@
position: relative;
inline-size: var(--comment-avatar-size);
block-size: var(--comment-avatar-size);
/* rounded so the admin ring (box-shadow) reads as a circle */
border-radius: var(--radius-full);
}
.comment-thread__avatar img {
@@ -3561,6 +3563,30 @@
display: block;
}
/* Pending-submissions dot on the Admin pane's "Submissions" subtab. The
subtabs sit in a horizontal row, so this dot rides the top-right corner
(not the left edge like the vertical nav dots). */
.admin-subtab-has-dot {
position: relative;
}
.admin-subtab-dot {
position: absolute;
top: calc(-1 * var(--space-3xs));
right: calc(-1 * var(--space-3xs));
width: var(--space-sm);
height: var(--space-sm);
border-radius: var(--radius-full);
border: var(--border-width-hairline) solid var(--surface-panel);
background-color: var(--status-danger);
display: none;
pointer-events: none;
}
.admin-subtab-dot.is-visible {
display: block;
}
.profile-modal__panes {
display: flex;
flex: 1 1 auto;
+4 -1
View File
@@ -10,7 +10,7 @@ import { subscriptions } from "./subscriptions.js";
import { attachHealthBadges } from "./gridHealth.js";
import { attachUrlHealthBadges } from "./urlHealthObserver.js";
import { accessControl } from "./accessControl.js";
import { applyAdminStar } from "./ui/adminBadge.js";
import { applyAdminStar, applyAdminRing } from "./ui/adminBadge.js";
import { getApplication } from "./applicationContext.js";
import { escapeHTML } from "./utils/domUtils.js";
import { formatShortNpub } from "./utils/formatters.js";
@@ -5161,6 +5161,9 @@ function applyChannelProfileMetadata({
const blurPubkey = pubkey || currentChannelHex || "";
applyAdminStar(document.getElementById("channelAvatarWrap"), blurPubkey);
// Ring the inner rounded avatar box (the wrap is a non-clipped positioning
// shell; the box-shadow ring belongs on the circular element itself).
applyAdminRing(avatarEl?.parentElement, blurPubkey);
updateChannelModerationVisuals({
bannerEl,
avatarEl,
+18
View File
@@ -90,6 +90,23 @@ export function applyAdminStar(
return setAdminStar(container, isAdminActor(pubkeyOrNpub), { doc, label });
}
/**
* Toggle just the gold ring on a circular element (for surfaces that already
* place the star on a specific element and only need the ring added). The
* element should be circular (border-radius) for the ring to read right.
* @param {Element|null} el
* @param {string} pubkeyOrNpub
* @returns {boolean} whether the ring is applied
*/
export function applyAdminRing(el, pubkeyOrNpub) {
if (!el || !el.classList) {
return false;
}
const isAdmin = isAdminActor(pubkeyOrNpub);
el.classList.toggle(ADMIN_RING_CLASS, isAdmin);
return isAdmin;
}
/**
* Decorate a circular avatar element with BOTH the admin ring and the corner
* star. `avatarEl` is the circular element (an <img> or a clip container). The
@@ -138,5 +155,6 @@ export default {
ADMIN_WRAP_CLASS,
isAdminActor,
applyAdminStar,
applyAdminRing,
decorateAdminAvatar,
};
+10
View File
@@ -879,6 +879,16 @@ export default class ApplicationBootstrap {
) {
app.profileController.setMessagesUnreadIndicator(app._dmUnread);
}
// Pending-submissions dots on the Admin tab + Submissions subtab so an
// admin knows exactly where to look.
if (
app.profileController &&
typeof app.profileController.setSubmissionsUnreadIndicator === "function"
) {
app.profileController.setSubmissionsUnreadIndicator(
app._submissionsPending,
);
}
};
app.refreshUnreadDmIndicator = async ({ reason = "" } = {}) => {
@@ -1,7 +1,7 @@
import { UI_FEEDBACK_DELAY_MS } from "../../../constants.js";
import { formatAbsoluteTimestamp, formatTimeAgo } from "../../../utils/formatters.js";
import { registerCommentAvatarFailure as registerCommentAvatarFailureUtil } from "./utils/commentAvatar.js";
import { applyAdminStar } from "../../adminBadge.js";
import { applyAdminStar, applyAdminRing } from "../../adminBadge.js";
export class CommentNodeFactory {
constructor({ document, window, logger, dispatch, DEFAULT_PROFILE_AVATAR }) {
@@ -46,11 +46,11 @@ export class CommentNodeFactory {
const avatarLabel = this.document.createElement("span");
avatarWrapper.appendChild(avatarLabel);
// Gold admin star for comments authored by a bitvid admin.
// Gold admin ring + star for comments authored by a bitvid admin.
if (typeof event.pubkey === "string" && event.pubkey.trim()) {
applyAdminStar(avatarWrapper, event.pubkey.trim(), {
doc: this.document,
});
const authorKey = event.pubkey.trim();
applyAdminRing(avatarWrapper, authorKey);
applyAdminStar(avatarWrapper, authorKey, { doc: this.document });
}
const content = this.document.createElement("div");
+26
View File
@@ -721,6 +721,12 @@ export class ProfileModalController {
this.navButtons.admin = document.getElementById("profileNavAdmin") || null;
this.navButtons.safety = document.getElementById("profileNavSafety") || null;
// Pending-submissions dots: on the Admin nav tab and the Submissions subtab.
this.adminNavUnreadDot =
document.getElementById("profileNavAdminUnreadDot") || null;
this.adminSubmissionsDot =
document.getElementById("adminSubtabSubmissionsDot") || null;
this.profileEditBtn = document.getElementById("profileEditBtn") || null;
this.profileEditBackBtn = document.getElementById("profileEditBackBtn") || null;
@@ -1353,6 +1359,26 @@ export class ProfileModalController {
);
}
// Pending-submissions dots for admins: on the Admin nav tab (gated on that tab
// being visible) and on the Submissions subtab button inside the admin pane.
setSubmissionsUnreadIndicator(visible) {
const show = Boolean(visible);
const adminButton = this.navButtons.admin;
const adminNavVisible =
adminButton instanceof HTMLElement &&
!adminButton.classList.contains("hidden") &&
!adminButton.hasAttribute("hidden");
if (this.adminNavUnreadDot instanceof HTMLElement) {
this.adminNavUnreadDot.classList.toggle(
"is-visible",
show && adminNavVisible,
);
}
if (this.adminSubmissionsDot instanceof HTMLElement) {
this.adminSubmissionsDot.classList.toggle("is-visible", show);
}
}