From 225c6a167433b0f590253f0854cc3ee76f38e157 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:57:10 -0500 Subject: [PATCH 1/2] Add unread DM badge to profile button --- css/tailwind.source.css | 17 ++++++++ index.html | 3 +- js/app.js | 6 +++ js/services/nostrService.js | 8 +++- js/ui/appChromeController.js | 16 ++++++++ js/ui/applicationBootstrap.js | 77 +++++++++++++++++++++++++++++++++-- 6 files changed, 121 insertions(+), 6 deletions(-) diff --git a/css/tailwind.source.css b/css/tailwind.source.css index e5b4a7df..76d274f2 100644 --- a/css/tailwind.source.css +++ b/css/tailwind.source.css @@ -235,6 +235,23 @@ display: block; } + #profileUnreadDot { + position: absolute; + top: var(--space-3xs); + right: var(--space-3xs); + width: calc(var(--space-2xs) + var(--space-3xs)); + height: calc(var(--space-2xs) + var(--space-3xs)); + border-radius: 999px; + border: var(--border-width-hairline) solid var(--color-page); + background-color: var(--status-danger); + display: none; + pointer-events: none; + } + + #profileUnreadDot.is-visible { + display: block; + } + .header-icon-toggle[data-theme-state="light"]:hover, .header-icon-toggle[data-theme-state="light"]:focus-visible { color: var(--color-text); diff --git a/index.html b/index.html index 6b0f8402..b145f4b2 100644 --- a/index.html +++ b/index.html @@ -290,7 +290,7 @@ diff --git a/js/app.js b/js/app.js index 271ddac0..c12ddfe8 100644 --- a/js/app.js +++ b/js/app.js @@ -4429,6 +4429,9 @@ class Application { this.applyAuthenticatedUiState(); this.commentController?.refreshAuthState?.(); + if (typeof this.refreshUnreadDmIndicator === "function") { + void this.refreshUnreadDmIndicator({ reason: "auth-login" }); + } const currentView = getHashViewName(); const normalizedView = @@ -4681,6 +4684,9 @@ class Application { } this.applyLoggedOutUiState(); + if (this.appChromeController?.setUnreadDmIndicator) { + this.appChromeController.setUnreadDmIndicator(false); + } const logoutView = getHashViewName(); if ( diff --git a/js/services/nostrService.js b/js/services/nostrService.js index 80a4ee79..f1f1f3e0 100644 --- a/js/services/nostrService.js +++ b/js/services/nostrService.js @@ -763,10 +763,16 @@ export class NostrService { } async acknowledgeRenderedDirectMessages(conversationId, renderedUntil) { - return this.dmNotificationManager.acknowledgeRenderedMessages({ + const conversation = await this.dmNotificationManager.acknowledgeRenderedMessages({ conversationId, renderedUntil, }); + if (conversation) { + this.emit("directMessages:conversationUpdated", { + conversation, + }); + } + return conversation; } getDirectMessageUnseenCount(conversationId) { diff --git a/js/ui/appChromeController.js b/js/ui/appChromeController.js index 1960bd20..a4e73e9c 100644 --- a/js/ui/appChromeController.js +++ b/js/ui/appChromeController.js @@ -18,6 +18,7 @@ export default class AppChromeController { this.elements = { logoutButton: this.normalizeElement(elements.logoutButton), profileButton: this.normalizeElement(elements.profileButton), + profileUnreadDot: this.normalizeElement(elements.profileUnreadDot), uploadButton: this.normalizeElement(elements.uploadButton), loginButton: this.normalizeElement(elements.loginButton), closeLoginModalButton: this.normalizeElement( @@ -206,6 +207,21 @@ export default class AppChromeController { } } + setUnreadDmIndicator(visible) { + const dot = this.normalizeElement(this.elements.profileUnreadDot); + if (!dot) { + return; + } + + const profileButton = this.normalizeElement(this.elements.profileButton); + const isProfileVisible = + profileButton && + !profileButton.classList.contains("hidden") && + !profileButton.hasAttribute("hidden"); + + dot.classList.toggle("is-visible", Boolean(visible) && isProfileVisible); + } + bindUploadButton() { const button = this.normalizeElement(this.elements.uploadButton); if (this.boundElements.uploadButton === button) { diff --git a/js/ui/applicationBootstrap.js b/js/ui/applicationBootstrap.js index 9d21d8f3..bdc21cc9 100644 --- a/js/ui/applicationBootstrap.js +++ b/js/ui/applicationBootstrap.js @@ -409,6 +409,7 @@ export default class ApplicationBootstrap { app.profileButton = doc?.getElementById("profileButton") || null; app.profileAvatar = doc?.getElementById("profileAvatar") || null; + app.profileUnreadDot = doc?.getElementById("profileUnreadDot") || null; const modalManager = new ModalManager({ app, @@ -685,12 +686,79 @@ export default class ApplicationBootstrap { }; app.videosMap = app.nostrService.getVideosMap(); - app.unsubscribeFromNostrService = app.nostrService.on( - "subscription:changed", - ({ subscription }) => { + app.refreshUnreadDmIndicator = async ({ reason = "" } = {}) => { + if ( + !app.appChromeController || + typeof app.appChromeController.setUnreadDmIndicator !== "function" + ) { + return; + } + + if (typeof app.isUserLoggedIn === "function" && !app.isUserLoggedIn()) { + app.appChromeController.setUnreadDmIndicator(false); + return; + } + + if ( + !app.nostrService || + typeof app.nostrService.listDirectMessageConversationSummaries !== + "function" + ) { + app.appChromeController.setUnreadDmIndicator(false); + return; + } + + try { + const summaries = + await app.nostrService.listDirectMessageConversationSummaries(); + const totalUnseen = Array.isArray(summaries) + ? summaries.reduce((sum, summary) => { + const count = Number(summary?.unseen_count); + return sum + (Number.isFinite(count) ? count : 0); + }, 0) + : 0; + app.appChromeController.setUnreadDmIndicator(totalUnseen > 0); + } catch (error) { + devLogger.warn( + "[Application] Failed to refresh DM unread indicator", + reason, + error, + ); + app.appChromeController.setUnreadDmIndicator(false); + } + }; + + const nostrUnsubscribes = []; + nostrUnsubscribes.push( + app.nostrService.on("subscription:changed", ({ subscription }) => { app.videoSubscription = subscription || null; - }, + }), ); + nostrUnsubscribes.push( + app.nostrService.on("directMessages:notification", () => { + void app.refreshUnreadDmIndicator({ reason: "dm-notification" }); + }), + ); + nostrUnsubscribes.push( + app.nostrService.on("directMessages:conversationUpdated", () => { + void app.refreshUnreadDmIndicator({ reason: "dm-conversation-update" }); + }), + ); + nostrUnsubscribes.push( + app.nostrService.on("directMessages:cleared", () => { + if (app.appChromeController) { + app.appChromeController.setUnreadDmIndicator(false); + } + }), + ); + app.unsubscribeFromNostrService = () => { + while (nostrUnsubscribes.length) { + const unsubscribe = nostrUnsubscribes.pop(); + if (typeof unsubscribe === "function") { + unsubscribe(); + } + } + }; app.boundNwcSettingsToastHandler = null; Object.defineProperty(app, "savedProfiles", { configurable: false, @@ -926,6 +994,7 @@ export default class ApplicationBootstrap { elements: { logoutButton: app.logoutButton, profileButton: app.profileButton, + profileUnreadDot: app.profileUnreadDot, uploadButton: app.uploadButton, loginButton: app.loginButton, closeLoginModalButton: app.closeLoginModalBtn, From d45d380e4040f0653bd1bdd2dcf9c3d4b236469c Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 19 Jan 2026 15:09:35 -0500 Subject: [PATCH 2/2] Improve DM unread indicators and profile modal layout --- components/profile-modal.html | 7 +++- css/tailwind.source.css | 65 ++++++++++++++++++++++++++++++--- css/tokens.css | 2 + js/app.js | 4 +- js/ui/applicationBootstrap.js | 33 +++++++++++------ js/ui/profileModalController.js | 21 +++++++++++ 6 files changed, 112 insertions(+), 20 deletions(-) diff --git a/components/profile-modal.html b/components/profile-modal.html index b9f4cee5..d68a86f5 100644 --- a/components/profile-modal.html +++ b/components/profile-modal.html @@ -148,11 +148,16 @@