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 @@