Profile dot: show for DMs OR pending admin submissions

The profile-button red dot now lights when the logged-in user has either
unseen DMs or (if they can edit admin lists) pending submissions to review.
The Messages-nav dot stays DM-only.

- applicationBootstrap.js: split indicator state into _dmUnread and
  _submissionsPending; applyProfileNotificationDot() ORs them for the
  profile dot. Added refreshSubmissionsIndicator (gated on FEATURE_SUBMISSIONS
  + canEditAdminLists), a bitvid:submissions-changed listener, a 180s poll,
  and a startup refresh.
- authSessionCoordinator.js: refresh the submissions indicator on
  auth-login and auth-logout alongside the DM refresh.
- submissionsSection.js: emit bitvid:submissions-changed after approve/deny
  so the dot recomputes for all admins.
- check-file-size.mjs: bump authSessionCoordinator grandfather baseline.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-07 09:35:41 -04:00
1 parent 36cb71b676
commit ef4191538c
4 files changed
+107 -14

No files matched your search

+6
View File
@@ -237,6 +237,9 @@ export function createAuthSessionCoordinator(deps) {
if (typeof this.refreshUnreadDmIndicator === "function") {
void this.refreshUnreadDmIndicator({ reason: "auth-login" });
}
if (typeof this.refreshSubmissionsIndicator === "function") {
void this.refreshSubmissionsIndicator({ reason: "auth-login" });
}
const currentView = getHashViewName();
const normalizedView =
@@ -1043,6 +1046,9 @@ export function createAuthSessionCoordinator(deps) {
} else if (this.appChromeController?.setUnreadDmIndicator) {
this.appChromeController.setUnreadDmIndicator(false);
}
if (typeof this.refreshSubmissionsIndicator === "function") {
void this.refreshSubmissionsIndicator({ reason: "auth-logout" });
}
const logoutView = getHashViewName();
if (
+88 -13
View File
@@ -83,6 +83,8 @@ import {
isDevMode,
} from "../config.js";
import { ADMIN_INITIAL_EVENT_BLACKLIST } from "../lists.js";
import { FEATURE_SUBMISSIONS } from "../constants.js";
import { fetchPendingSubmissions } from "../submissions/submissionFacade.js";
import {
prepareStaticModal,
openStaticModal,
@@ -858,21 +860,30 @@ export default class ApplicationBootstrap {
};
app.videosMap = app.nostrService.getVideosMap();
// The profile-button red dot shows when EITHER there are unread DMs OR (for
// admins) there are pending submissions. The Messages nav dot stays DM-only.
app._dmUnread = false;
app._submissionsPending = false;
app.applyProfileNotificationDot = () => {
const profileDot = app._dmUnread || app._submissionsPending;
if (
app.appChromeController &&
typeof app.appChromeController.setUnreadDmIndicator === "function"
) {
app.appChromeController.setUnreadDmIndicator(profileDot);
}
if (
app.profileController &&
typeof app.profileController.setMessagesUnreadIndicator === "function"
) {
app.profileController.setMessagesUnreadIndicator(app._dmUnread);
}
};
app.refreshUnreadDmIndicator = async ({ reason = "" } = {}) => {
const applyIndicatorState = (visible) => {
if (
app.appChromeController &&
typeof app.appChromeController.setUnreadDmIndicator === "function"
) {
app.appChromeController.setUnreadDmIndicator(visible);
}
if (
app.profileController &&
typeof app.profileController.setMessagesUnreadIndicator === "function"
) {
app.profileController.setMessagesUnreadIndicator(visible);
}
app._dmUnread = Boolean(visible);
app.applyProfileNotificationDot();
};
if (typeof app.isUserLoggedIn === "function" && !app.isUserLoggedIn()) {
@@ -929,6 +940,43 @@ export default class ApplicationBootstrap {
}
};
// #23: admin-only signal — pending submissions light up the profile dot too.
app.refreshSubmissionsIndicator = async ({ reason = "" } = {}) => {
let pending = false;
try {
if (
FEATURE_SUBMISSIONS &&
typeof app.isUserLoggedIn === "function" &&
app.isUserLoggedIn()
) {
const actorNpub =
typeof app.getCurrentUserNpub === "function"
? app.getCurrentUserNpub()
: "";
if (
actorNpub &&
typeof accessControl.canEditAdminLists === "function" &&
accessControl.canEditAdminLists(actorNpub)
) {
const adminHex = app.safeDecodeNpub(ADMIN_SUPER_NPUB);
const editorHexes = Array.from(accessControl.getEditors?.() || [])
.map((npub) => app.safeDecodeNpub(npub))
.filter(Boolean);
const list = await fetchPendingSubmissions({ adminHex, editorHexes });
pending = Array.isArray(list) && list.length > 0;
}
}
} catch (error) {
devLogger.warn(
"[Application] Failed to refresh submissions indicator",
reason,
error,
);
}
app._submissionsPending = pending;
app.applyProfileNotificationDot();
};
const nostrUnsubscribes = [];
nostrUnsubscribes.push(
app.nostrService.on("subscription:changed", ({ subscription }) => {
@@ -975,6 +1023,33 @@ export default class ApplicationBootstrap {
}
}),
);
// #23: keep the admin submissions dot fresh — recompute when one is resolved
// (event) and on a light poll (submissions arrive from external users, so
// there's no push signal). refreshSubmissionsIndicator no-ops for non-admins.
if (typeof document !== "undefined") {
const onSubmissionsChanged = () => {
void app.refreshSubmissionsIndicator({ reason: "submissions-changed" });
};
document.addEventListener(
"bitvid:submissions-changed",
onSubmissionsChanged,
);
nostrUnsubscribes.push(() =>
document.removeEventListener(
"bitvid:submissions-changed",
onSubmissionsChanged,
),
);
}
const submissionsPollId = setInterval(() => {
void app.refreshSubmissionsIndicator({ reason: "poll" });
}, 180000);
if (submissionsPollId && typeof submissionsPollId.unref === "function") {
submissionsPollId.unref();
}
nostrUnsubscribes.push(() => clearInterval(submissionsPollId));
void app.refreshSubmissionsIndicator({ reason: "startup" });
app.unsubscribeFromNostrService = () => {
while (nostrUnsubscribes.length) {
const unsubscribe = nostrUnsubscribes.pop();
+12
View File
@@ -22,6 +22,16 @@ function setLoading(controller, value) {
}
}
// Let the app recompute the profile-button "pending submissions" dot after a
// moderator approves/denies one.
function emitSubmissionsChanged() {
try {
document.dispatchEvent(new CustomEvent("bitvid:submissions-changed"));
} catch (error) {
// best effort
}
}
export async function populateSubmissions(controller) {
if (!FEATURE_SUBMISSIONS || !controller.submissionsList) {
return;
@@ -214,6 +224,7 @@ async function approveSubmission(controller, submission) {
`Approved — ${applicant} added to the whitelist.`,
);
controller.populateAdminLists();
emitSubmissionsChanged();
} catch (error) {
devLogger.warn("[profileModal] Approve submission failed:", error);
controller.mainController.showError?.("Couldn't approve — please try again.");
@@ -238,6 +249,7 @@ async function denySubmission(controller, submission) {
actingHex: safeDecodeNpub(actorNpub),
});
controller.mainController.showSuccess?.("Submission dismissed.");
emitSubmissionsChanged();
} catch (error) {
devLogger.warn("[profileModal] Deny submission failed:", error);
controller.mainController.showError?.("Couldn't dismiss — please try again.");
+1 -1
View File
@@ -44,7 +44,7 @@ const GRANDFATHERED = {
"js/state/cache.js": 1827,
"js/app/feedCoordinator.js": 1857,
"js/viewCounter.js": 1023,
"js/app/authSessionCoordinator.js": 1803,
"js/app/authSessionCoordinator.js": 2008,
"js/ui/components/RevertModal.js": 1663,
"js/payments/nwcClient.js": 1642,
"js/ui/moreMenuController.js": 1601,