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 @@ Messages + + diff --git a/js/app.js b/js/app.js index 271ddac0..170d03d5 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,11 @@ class Application { } this.applyLoggedOutUiState(); + if (typeof this.refreshUnreadDmIndicator === "function") { + void this.refreshUnreadDmIndicator({ reason: "auth-logout" }); + } else 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..1a00c3b6 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,88 @@ export default class ApplicationBootstrap { }; app.videosMap = app.nostrService.getVideosMap(); - app.unsubscribeFromNostrService = app.nostrService.on( - "subscription:changed", - ({ subscription }) => { + 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); + } + }; + + if (typeof app.isUserLoggedIn === "function" && !app.isUserLoggedIn()) { + applyIndicatorState(false); + return; + } + + if ( + !app.nostrService || + typeof app.nostrService.listDirectMessageConversationSummaries !== + "function" + ) { + applyIndicatorState(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; + applyIndicatorState(totalUnseen > 0); + } catch (error) { + devLogger.warn( + "[Application] Failed to refresh DM unread indicator", + reason, + error, + ); + applyIndicatorState(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 (typeof app.refreshUnreadDmIndicator === "function") { + void app.refreshUnreadDmIndicator({ reason: "dm-cleared" }); + } + }), + ); + 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 +1003,7 @@ export default class ApplicationBootstrap { elements: { logoutButton: app.logoutButton, profileButton: app.profileButton, + profileUnreadDot: app.profileUnreadDot, uploadButton: app.uploadButton, loginButton: app.loginButton, closeLoginModalButton: app.closeLoginModalBtn, diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js index cd164bf2..e5a78276 100644 --- a/js/ui/profileModalController.js +++ b/js/ui/profileModalController.js @@ -1297,6 +1297,7 @@ export class ProfileModalController { this.profileMessagesRelayStatus = null; this.profileMessagesReadReceiptsToggle = null; this.profileMessagesTypingToggle = null; + this.profileMessagesUnreadDot = null; this.dmAppShellContainer = null; this.dmAppShell = null; this.profileLinkPreviewAutoToggle = null; @@ -1615,6 +1616,8 @@ export class ProfileModalController { document.getElementById("profileMessagesReadReceiptsToggle") || null; this.profileMessagesTypingToggle = document.getElementById("profileMessagesTypingToggle") || null; + this.profileMessagesUnreadDot = + document.getElementById("profileMessagesUnreadDot") || null; this.dmAppShellContainer = document.getElementById("dmAppShellMount") || null; this.profileLinkPreviewAutoToggle = @@ -2758,6 +2761,23 @@ export class ProfileModalController { } } + setMessagesUnreadIndicator(visible) { + if (!(this.profileMessagesUnreadDot instanceof HTMLElement)) { + return; + } + + const button = this.navButtons.messages; + const isVisible = + button instanceof HTMLElement && + !button.classList.contains("hidden") && + !button.hasAttribute("hidden"); + + this.profileMessagesUnreadDot.classList.toggle( + "is-visible", + Boolean(visible) && isVisible, + ); + } + updateMessageComposerState() { const input = this.profileMessageInput; const button = this.profileMessageSendButton; @@ -12593,6 +12613,7 @@ export class ProfileModalController { this.clearHashtagInputs(); this.setHashtagStatus("", "muted"); this.handleActiveDmIdentityChanged(null); + this.setMessagesUnreadIndicator(false); this.populateDmRelayPreferences(); this.setDmRelayPreferencesStatus("");