From ef4191538c9ee4c89549f9637a20ce14d78751d9 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:35:41 -0400 Subject: [PATCH] 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 --- js/app/authSessionCoordinator.js | 6 ++ js/ui/applicationBootstrap.js | 101 ++++++++++++++++++++--- js/ui/profileModal/submissionsSection.js | 12 +++ scripts/check-file-size.mjs | 2 +- 4 files changed, 107 insertions(+), 14 deletions(-) diff --git a/js/app/authSessionCoordinator.js b/js/app/authSessionCoordinator.js index ea3059e3..36e4fc91 100644 --- a/js/app/authSessionCoordinator.js +++ b/js/app/authSessionCoordinator.js @@ -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 ( diff --git a/js/ui/applicationBootstrap.js b/js/ui/applicationBootstrap.js index 89f67f60..69cd119e 100644 --- a/js/ui/applicationBootstrap.js +++ b/js/ui/applicationBootstrap.js @@ -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(); diff --git a/js/ui/profileModal/submissionsSection.js b/js/ui/profileModal/submissionsSection.js index 0ee59e3b..e32e4dbd 100644 --- a/js/ui/profileModal/submissionsSection.js +++ b/js/ui/profileModal/submissionsSection.js @@ -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."); diff --git a/scripts/check-file-size.mjs b/scripts/check-file-size.mjs index b8097b93..dfc1c38e 100644 --- a/scripts/check-file-size.mjs +++ b/scripts/check-file-size.mjs @@ -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,