diff --git a/js/app.js b/js/app.js index 48da3666..e747d190 100644 --- a/js/app.js +++ b/js/app.js @@ -3494,13 +3494,32 @@ class Application { return false; } - const sessionActorPubkey = this.normalizeHexPubkey( - nostrClient?.sessionActor?.pubkey, - ); - if (sessionActorPubkey && sessionActorPubkey === normalizedPubkey) { + const clientPubkey = this.normalizeHexPubkey(nostrClient?.pubkey); + if (clientPubkey && clientPubkey !== normalizedPubkey) { return false; } + const sessionActor = nostrClient?.sessionActor || null; + const sessionActorPubkey = this.normalizeHexPubkey(sessionActor?.pubkey); + if (sessionActorPubkey && sessionActorPubkey !== normalizedPubkey) { + const rawPrivateKey = + typeof sessionActor?.privateKey === "string" + ? sessionActor.privateKey.trim() + : ""; + const hasEmbeddedPrivateKey = + rawPrivateKey.length > 0 && HEX64_REGEX.test(rawPrivateKey); + + const declaredSource = + typeof sessionActor?.source === "string" + ? sessionActor.source.trim() + : ""; + const isPersisted = sessionActor?.persisted === true; + + if (!hasEmbeddedPrivateKey || declaredSource || isPersisted) { + return false; + } + } + return true; } @@ -3588,6 +3607,34 @@ class Application { } } + refreshChromeElements() { + const doc = typeof document !== "undefined" ? document : null; + const findElement = (id) => { + if (!doc || typeof doc.getElementById !== "function") { + return null; + } + const element = doc.getElementById(id); + if (!element || typeof element.addEventListener !== "function") { + return null; + } + return element; + }; + + this.loginButton = findElement("loginButton"); + this.logoutButton = findElement("logoutButton"); + this.uploadButton = findElement("uploadButton"); + this.profileButton = findElement("profileButton"); + this.closeLoginModalBtn = findElement("closeLoginModal"); + + return { + loginButton: this.loginButton, + logoutButton: this.logoutButton, + uploadButton: this.uploadButton, + profileButton: this.profileButton, + closeLoginModalButton: this.closeLoginModalBtn, + }; + } + resolveSubscriptionsLink() { if (this.subscriptionsLink instanceof HTMLElement) { return this.subscriptionsLink; @@ -3604,7 +3651,20 @@ class Application { } hydrateSidebarNavigation() { + const chromeElements = this.refreshChromeElements(); this.resolveSubscriptionsLink(); + + if (this.appChromeController) { + if (typeof this.appChromeController.setElements === "function") { + this.appChromeController.setElements(chromeElements); + } else { + if (this.appChromeController.elements) { + Object.assign(this.appChromeController.elements, chromeElements); + } + this.appChromeController.initialize(); + } + } + this.syncAuthUiState(); } diff --git a/js/ui/appChromeController.js b/js/ui/appChromeController.js index f8cb7467..1960bd20 100644 --- a/js/ui/appChromeController.js +++ b/js/ui/appChromeController.js @@ -16,11 +16,13 @@ export default class AppChromeController { logger, } = {}) { this.elements = { - logoutButton: elements.logoutButton || null, - profileButton: elements.profileButton || null, - uploadButton: elements.uploadButton || null, - loginButton: elements.loginButton || null, - closeLoginModalButton: elements.closeLoginModalButton || null, + logoutButton: this.normalizeElement(elements.logoutButton), + profileButton: this.normalizeElement(elements.profileButton), + uploadButton: this.normalizeElement(elements.uploadButton), + loginButton: this.normalizeElement(elements.loginButton), + closeLoginModalButton: this.normalizeElement( + elements.closeLoginModalButton, + ), }; this.callbacks = { @@ -59,42 +61,17 @@ export default class AppChromeController { this.logger = logger || defaultLogger; this.isInitialized = false; + this.hasBoundGlobalEvents = false; - this.handleBeforeUnload = this.handleBeforeUnload.bind(this); - this.handleVisibilityChange = this.handleVisibilityChange.bind(this); - this.handlePopstate = this.handlePopstate.bind(this); - this.handleDocumentClick = this.handleDocumentClick.bind(this); - } + this.boundElements = { + logoutButton: null, + profileButton: null, + uploadButton: null, + loginButton: null, + closeLoginModalButton: null, + }; - initialize() { - if (this.isInitialized) { - return; - } - - this.bindLogoutButton(); - this.bindProfileButton(); - this.bindUploadButton(); - this.bindLoginButton(); - this.bindCloseLoginButton(); - this.bindGlobalEvents(); - - this.isInitialized = true; - } - - getTriggerFromEvent(event) { - if (!event) { - return null; - } - return event.currentTarget || event.target || null; - } - - bindLogoutButton() { - const { logoutButton } = this.elements; - if (!logoutButton || typeof logoutButton.addEventListener !== "function") { - return; - } - - logoutButton.addEventListener("click", async () => { + this.handleLogoutClick = async () => { try { if (typeof this.callbacks.requestLogout === "function") { await this.callbacks.requestLogout(); @@ -107,16 +84,9 @@ export default class AppChromeController { this.callbacks.showError(DEFAULT_LOGOUT_ERROR_MESSAGE); } } - }); - } + }; - bindProfileButton() { - const { profileButton } = this.elements; - if (!profileButton || typeof profileButton.addEventListener !== "function") { - return; - } - - profileButton.addEventListener("click", () => { + this.handleProfileClick = () => { if (typeof this.callbacks.showProfileModal !== "function") { return; } @@ -128,19 +98,13 @@ export default class AppChromeController { this.logger.error("Failed to open profile modal:", error); } }); - }); - } + }; - bindUploadButton() { - const { uploadButton } = this.elements; - if (!uploadButton || typeof uploadButton.addEventListener !== "function") { - return; - } - - uploadButton.addEventListener("click", (event) => { + this.handleUploadClick = (event) => { if (typeof this.callbacks.openUploadModal !== "function") { return; } + const triggerElement = this.getTriggerFromEvent(event); try { this.callbacks.openUploadModal({ triggerElement }); @@ -149,16 +113,9 @@ export default class AppChromeController { this.logger.error("Failed to open upload modal:", error); } } - }); - } + }; - bindLoginButton() { - const { loginButton } = this.elements; - if (!loginButton || typeof loginButton.addEventListener !== "function") { - return; - } - - loginButton.addEventListener("click", (event) => { + this.handleLoginClick = (event) => { if (this.logger?.log) { this.logger.log("Login button clicked!"); } @@ -178,19 +135,9 @@ export default class AppChromeController { this.callbacks.onLoginModalOpened(); } } - }); - } + }; - bindCloseLoginButton() { - const { closeLoginModalButton } = this.elements; - if ( - !closeLoginModalButton || - typeof closeLoginModalButton.addEventListener !== "function" - ) { - return; - } - - closeLoginModalButton.addEventListener("click", () => { + this.handleCloseLoginClick = () => { if (this.logger?.log) { this.logger.log("[app.js] closeLoginModal button clicked!"); } @@ -201,7 +148,149 @@ export default class AppChromeController { this.callbacks.onLoginModalClosed(); } } - }); + }; + + this.handleBeforeUnload = this.handleBeforeUnload.bind(this); + this.handleVisibilityChange = this.handleVisibilityChange.bind(this); + this.handlePopstate = this.handlePopstate.bind(this); + this.handleDocumentClick = this.handleDocumentClick.bind(this); + } + + initialize() { + this.bindLogoutButton(); + this.bindProfileButton(); + this.bindUploadButton(); + this.bindLoginButton(); + this.bindCloseLoginButton(); + + if (!this.hasBoundGlobalEvents) { + this.bindGlobalEvents(); + this.hasBoundGlobalEvents = true; + } + + this.isInitialized = true; + } + + getTriggerFromEvent(event) { + if (!event) { + return null; + } + return event.currentTarget || event.target || null; + } + + bindLogoutButton() { + const button = this.normalizeElement(this.elements.logoutButton); + if (this.boundElements.logoutButton === button) { + return; + } + + this.detachBoundElement("logoutButton", this.handleLogoutClick); + + if (button) { + button.addEventListener("click", this.handleLogoutClick); + this.boundElements.logoutButton = button; + } + } + + bindProfileButton() { + const button = this.normalizeElement(this.elements.profileButton); + if (this.boundElements.profileButton === button) { + return; + } + + this.detachBoundElement("profileButton", this.handleProfileClick); + + if (button) { + button.addEventListener("click", this.handleProfileClick); + this.boundElements.profileButton = button; + } + } + + bindUploadButton() { + const button = this.normalizeElement(this.elements.uploadButton); + if (this.boundElements.uploadButton === button) { + return; + } + + this.detachBoundElement("uploadButton", this.handleUploadClick); + + if (button) { + button.addEventListener("click", this.handleUploadClick); + this.boundElements.uploadButton = button; + } + } + + bindLoginButton() { + const button = this.normalizeElement(this.elements.loginButton); + if (this.boundElements.loginButton === button) { + return; + } + + this.detachBoundElement("loginButton", this.handleLoginClick); + + if (button) { + button.addEventListener("click", this.handleLoginClick); + this.boundElements.loginButton = button; + } + } + + bindCloseLoginButton() { + const button = this.normalizeElement(this.elements.closeLoginModalButton); + if (this.boundElements.closeLoginModalButton === button) { + return; + } + + this.detachBoundElement("closeLoginModalButton", this.handleCloseLoginClick); + + if (button) { + button.addEventListener("click", this.handleCloseLoginClick); + this.boundElements.closeLoginModalButton = button; + } + } + + setElements(nextElements = {}) { + if (!nextElements || typeof nextElements !== "object") { + return; + } + + const keys = [ + "logoutButton", + "profileButton", + "uploadButton", + "loginButton", + "closeLoginModalButton", + ]; + + let shouldRebind = false; + + for (const key of keys) { + if (Object.prototype.hasOwnProperty.call(nextElements, key)) { + const normalized = this.normalizeElement(nextElements[key]); + if (this.elements[key] !== normalized) { + this.elements[key] = normalized; + shouldRebind = true; + } + } + } + + if (shouldRebind) { + this.initialize(); + } + } + + detachBoundElement(key, handler) { + const previous = this.boundElements[key]; + if (previous && typeof previous.removeEventListener === "function") { + previous.removeEventListener("click", handler); + } + this.boundElements[key] = null; + } + + normalizeElement(element) { + if (!element || typeof element.addEventListener !== "function") { + return null; + } + return element; } bindGlobalEvents() { diff --git a/tests/app/hydrate-sidebar-navigation.test.mjs b/tests/app/hydrate-sidebar-navigation.test.mjs new file mode 100644 index 00000000..139e7e51 --- /dev/null +++ b/tests/app/hydrate-sidebar-navigation.test.mjs @@ -0,0 +1,96 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { JSDOM } from "jsdom"; + +import { Application } from "../../js/app.js"; + +const VIEW_HTML = ` + +
+