diff --git a/js/app.js b/js/app.js index 214684dd..d8b5578d 100644 --- a/js/app.js +++ b/js/app.js @@ -1531,18 +1531,11 @@ class Application { */ async initProfileModal() { try { - console.log("Starting profile modal initialization..."); - const resp = await fetch("components/profile-modal.html"); - if (!resp.ok) { - // If you don't have a profile modal, comment this entire method out. - throw new Error(`HTTP error! status: ${resp.status}`); - } - const html = await resp.text(); - const modalContainer = document.getElementById("modalContainer"); if (!modalContainer) { throw new Error("Modal container element not found!"); } + if (!this.profileModalController) { this.profileModalController = new ProfileModalController({ modalContainer, @@ -1556,376 +1549,99 @@ class Application { state: this.getProfileModalStateBridge(), }); } - const wrapper = document.createElement("div"); - wrapper.innerHTML = html; - removeTrackingScripts(wrapper); - modalContainer.appendChild(wrapper); - // Now references - this.profileModal = document.getElementById("profileModal") || null; - this.closeProfileModal = - document.getElementById("closeProfileModal") || null; - this.profileLogoutBtn = - document.getElementById("profileLogoutBtn") || null; - this.profileModalAvatar = - document.getElementById("profileModalAvatar") || null; - this.profileModalName = - document.getElementById("profileModalName") || null; - this.profileModalNpub = - document.getElementById("profileModalNpub") || null; - this.profileChannelLink = - document.getElementById("profileChannelLink") || null; - this.profileSwitcherList = - document.getElementById("profileSwitcherList") || null; - this.profileAddAccountBtn = - document.getElementById("profileAddAccountBtn") || null; - const topLevelProfileAvatar = - document.getElementById("profileAvatar") || null; - if (topLevelProfileAvatar) { - this.profileAvatar = topLevelProfileAvatar; - } - this.profileNavButtons.account = - document.getElementById("profileNavAccount") || null; - this.profileNavButtons.relays = - document.getElementById("profileNavRelays") || null; - this.profileNavButtons.wallet = - document.getElementById("profileNavWallet") || null; - this.profileNavButtons.blocked = - document.getElementById("profileNavBlocked") || null; - this.profileNavButtons.history = - document.getElementById("profileNavHistory") || null; - this.profileNavButtons.admin = - document.getElementById("profileNavAdmin") || null; - this.profilePaneElements.account = - document.getElementById("profilePaneAccount") || null; - this.profilePaneElements.relays = - document.getElementById("profilePaneRelays") || null; - this.profilePaneElements.wallet = - document.getElementById("profilePaneWallet") || null; - this.profilePaneElements.blocked = - document.getElementById("profilePaneBlocked") || null; - this.profilePaneElements.history = - document.getElementById("profilePaneHistory") || null; - this.profilePaneElements.admin = - document.getElementById("profilePaneAdmin") || null; - this.profileRelayList = document.getElementById("relayList") || null; - this.profileBlockedList = document.getElementById("blockedList") || null; - this.profileBlockedEmpty = - document.getElementById("blockedEmpty") || null; - this.profileBlockedInput = - document.getElementById("blockedInput") || null; - this.profileAddBlockedBtn = - document.getElementById("addBlockedBtn") || null; - this.profileRelayInput = document.getElementById("relayInput") || null; - this.profileAddRelayBtn = document.getElementById("addRelayBtn") || null; - this.profileRestoreRelaysBtn = - document.getElementById("restoreRelaysBtn") || null; - this.profileWalletUriInput = - document.getElementById("profileWalletUri") || null; - this.profileWalletDefaultZapInput = - document.getElementById("profileWalletDefaultZap") || null; - this.profileWalletSaveButton = - document.getElementById("profileWalletSave") || null; - this.profileWalletTestButton = - document.getElementById("profileWalletTest") || null; - this.profileWalletDisconnectButton = - document.getElementById("profileWalletDisconnect") || null; - this.profileWalletStatusText = - document.getElementById("profileWalletStatus") || null; - if (!this.profileHistoryRenderer) { - this.profileHistoryRenderer = createWatchHistoryRenderer({ - viewSelector: "#profilePaneHistory", - gridSelector: "#profileHistoryGrid", - loadingSelector: "#profileHistoryLoading", - statusSelector: "#profileHistoryStatus", - emptySelector: "#profileHistoryEmpty", - sentinelSelector: "#profileHistorySentinel", - scrollContainerSelector: "#profileHistoryScroll", - errorBannerSelector: "#profileHistoryError", - clearButtonSelector: "#profileHistoryClear", - republishButtonSelector: "#profileHistoryRepublish", - featureBannerSelector: "#profileHistoryFeatureBanner", - toastRegionSelector: "#profileHistoryToastRegion", - sessionWarningSelector: "#profileHistorySessionWarning", - metadataToggleSelector: "#profileHistoryMetadataToggle", - metadataThumbSelector: "#profileHistoryMetadataThumb", - metadataLabelSelector: "#profileHistoryMetadataLabel", - metadataDescriptionSelector: "#profileHistoryMetadataDescription", - emptyCopy: "You haven’t watched any videos yet.", - remove: (payload) => this.handleWatchHistoryRemoval(payload), - getActor: async () => { - if (this.pubkey) { - return this.pubkey; - } - if ( - typeof nostrClient?.sessionActor?.pubkey === "string" && - nostrClient.sessionActor.pubkey - ) { - return nostrClient.sessionActor.pubkey; - } - return this.pubkey || undefined; - }, - }); - } - this.adminModeratorsSection = - document.getElementById("adminModeratorsSection") || null; - this.adminModeratorsEmpty = - document.getElementById("adminModeratorsEmpty") || null; - this.adminModeratorList = - document.getElementById("adminModeratorList") || null; - this.adminModeratorInput = - document.getElementById("adminModeratorInput") || null; - this.adminAddModeratorBtn = - document.getElementById("adminAddModeratorBtn") || null; - this.adminWhitelistSection = - document.getElementById("adminWhitelistSection") || null; - this.adminWhitelistEmpty = - document.getElementById("adminWhitelistEmpty") || null; - this.adminWhitelistList = - document.getElementById("adminWhitelistList") || null; - this.adminWhitelistInput = - document.getElementById("adminWhitelistInput") || null; - this.adminAddWhitelistBtn = - document.getElementById("adminAddWhitelistBtn") || null; - this.adminBlacklistSection = - document.getElementById("adminBlacklistSection") || null; - this.adminBlacklistEmpty = - document.getElementById("adminBlacklistEmpty") || null; - this.adminBlacklistList = - document.getElementById("adminBlacklistList") || null; - this.adminBlacklistInput = - document.getElementById("adminBlacklistInput") || null; - this.adminAddBlacklistBtn = - document.getElementById("adminAddBlacklistBtn") || null; - - // Wire up - if (this.closeProfileModal && !this.closeProfileModal.dataset.bound) { - this.closeProfileModal.dataset.bound = "true"; - this.closeProfileModal.addEventListener("click", () => { - this.hideProfileModal(); - }); - } - if (this.profileLogoutBtn && !this.profileLogoutBtn.dataset.bound) { - this.profileLogoutBtn.dataset.bound = "true"; - this.profileLogoutBtn.addEventListener("click", async () => { - try { - await this.authService.logout(); - } catch (error) { - console.error("Logout failed:", error); - this.showError("Failed to logout. Please try again."); - } - this.hideProfileModal(); - }); - } - - if (this.profileChannelLink && !this.profileChannelLink.dataset.bound) { - this.profileChannelLink.dataset.bound = "true"; - this.profileChannelLink.addEventListener("click", (event) => { - event.preventDefault(); - const targetNpub = this.profileChannelLink?.dataset?.targetNpub; - if (!targetNpub) { - return; - } - this.hideProfileModal(); - window.location.hash = `#view=channel-profile&npub=${targetNpub}`; - }); - } - - if ( - this.profileAddAccountBtn && - this.profileAddAccountBtn.dataset.bound !== "true" - ) { - this.profileAddAccountBtn.dataset.bound = "true"; - this.profileAddAccountBtn.addEventListener("click", () => { - this.handleAddProfile(); - }); - } - - Object.entries(this.profileNavButtons).forEach(([name, button]) => { - if (!button || button.dataset.navBound === "true") { - return; + const alreadyLoaded = + this.profileModalController.profileModal instanceof HTMLElement; + if (!alreadyLoaded) { + const loaded = await this.profileModalController.load(); + if (!loaded) { + return false; } - button.dataset.navBound = "true"; - button.addEventListener("click", () => { - this.selectProfilePane(name); - }); + } else { + this.profileModalController.cacheDomReferences(); + } + + this.profileModal = this.profileModalController.profileModal; + this.closeProfileModal = this.profileModalController.closeButton; + this.profileLogoutBtn = this.profileModalController.logoutButton; + this.profileChannelLink = this.profileModalController.channelLink; + this.profileModalAvatar = this.profileModalController.profileModalAvatar; + this.profileModalName = this.profileModalController.profileModalName; + this.profileModalNpub = this.profileModalController.profileModalNpub; + this.profileSwitcherList = this.profileModalController.profileSwitcherList; + this.profileAddAccountBtn = this.profileModalController.addAccountButton; + this.profileAvatar = this.profileModalController.profileAvatar; + + Object.entries(this.profileNavButtons).forEach(([key]) => { + this.profileNavButtons[key] = + this.profileModalController.navButtons[key] || null; }); - if (this.profileAddRelayBtn && !this.profileAddRelayBtn.dataset.bound) { - this.profileAddRelayBtn.dataset.bound = "true"; - this.profileAddRelayBtn.addEventListener("click", () => { - this.handleAddRelay(); - }); - } - if ( - this.profileRestoreRelaysBtn && - this.profileRestoreRelaysBtn.dataset.bound !== "true" - ) { - this.profileRestoreRelaysBtn.dataset.bound = "true"; - this.profileRestoreRelaysBtn.addEventListener("click", () => { - this.handleRestoreRelays(); - }); - } - if ( - this.profileRelayInput && - this.profileRelayInput.dataset.bound !== "true" - ) { - this.profileRelayInput.dataset.bound = "true"; - this.profileRelayInput.addEventListener("keydown", (event) => { - if (event.key === "Enter") { - event.preventDefault(); - this.handleAddRelay(); - } - }); - } + Object.entries(this.profilePaneElements).forEach(([key]) => { + this.profilePaneElements[key] = + this.profileModalController.panes[key] || null; + }); - if ( - this.profileAddBlockedBtn && - this.profileAddBlockedBtn.dataset.bound !== "true" - ) { - this.profileAddBlockedBtn.dataset.bound = "true"; - this.profileAddBlockedBtn.addEventListener("click", () => { - this.handleAddBlockedCreator(); - }); - } - if ( - this.profileBlockedInput && - this.profileBlockedInput.dataset.bound !== "true" - ) { - this.profileBlockedInput.dataset.bound = "true"; - this.profileBlockedInput.addEventListener("keydown", (event) => { - if (event.key === "Enter") { - event.preventDefault(); - this.handleAddBlockedCreator(); - } - }); - } + this.profileRelayList = this.profileModalController.profileRelayList; + this.profileRelayInput = this.profileModalController.profileRelayInput; + this.profileAddRelayBtn = this.profileModalController.profileAddRelayBtn; + this.profileRestoreRelaysBtn = + this.profileModalController.profileRestoreRelaysBtn; - if ( - this.profileWalletUriInput && - this.profileWalletUriInput.dataset.bound !== "true" - ) { - this.profileWalletUriInput.dataset.bound = "true"; - this.profileWalletUriInput.addEventListener("input", () => { - this.applyWalletControlState(); - }); - } - if ( - this.profileWalletDefaultZapInput && - this.profileWalletDefaultZapInput.dataset.bound !== "true" - ) { - this.profileWalletDefaultZapInput.dataset.bound = "true"; - this.profileWalletDefaultZapInput.addEventListener("input", () => { - this.applyWalletControlState(); - }); - } - if ( - this.profileWalletSaveButton && - this.profileWalletSaveButton.dataset.bound !== "true" - ) { - this.profileWalletSaveButton.dataset.bound = "true"; - this.profileWalletSaveButton.addEventListener("click", () => { - this.handleWalletSave(); - }); - } - if ( - this.profileWalletTestButton && - this.profileWalletTestButton.dataset.bound !== "true" - ) { - this.profileWalletTestButton.dataset.bound = "true"; - this.profileWalletTestButton.addEventListener("click", () => { - this.handleWalletTest(); - }); - } - if ( - this.profileWalletDisconnectButton && - this.profileWalletDisconnectButton.dataset.bound !== "true" - ) { - this.profileWalletDisconnectButton.dataset.bound = "true"; - this.profileWalletDisconnectButton.addEventListener("click", () => { - this.handleWalletDisconnect(); - }); - } + this.profileBlockedList = this.profileModalController.profileBlockedList; + this.profileBlockedEmpty = this.profileModalController.profileBlockedEmpty; + this.profileBlockedInput = this.profileModalController.profileBlockedInput; + this.profileAddBlockedBtn = this.profileModalController.profileAddBlockedBtn; - if ( - this.adminAddModeratorBtn && - this.adminAddModeratorBtn.dataset.bound !== "true" - ) { - this.adminAddModeratorBtn.dataset.bound = "true"; - this.adminAddModeratorBtn.addEventListener("click", () => { - this.handleAddModerator(); - }); - } - if ( - this.adminModeratorInput && - this.adminModeratorInput.dataset.bound !== "true" - ) { - this.adminModeratorInput.dataset.bound = "true"; - this.adminModeratorInput.addEventListener("keydown", (event) => { - if (event.key === "Enter") { - event.preventDefault(); - this.handleAddModerator(); - } - }); - } + this.profileWalletUriInput = this.profileModalController.walletUriInput; + this.profileWalletDefaultZapInput = + this.profileModalController.walletDefaultZapInput; + this.profileWalletSaveButton = this.profileModalController.walletSaveButton; + this.profileWalletTestButton = this.profileModalController.walletTestButton; + this.profileWalletDisconnectButton = + this.profileModalController.walletDisconnectButton; + this.profileWalletStatusText = + this.profileModalController.profileWalletStatusText; - if ( - this.adminAddWhitelistBtn && - this.adminAddWhitelistBtn.dataset.bound !== "true" - ) { - this.adminAddWhitelistBtn.dataset.bound = "true"; - this.adminAddWhitelistBtn.addEventListener("click", () => { - this.handleAdminListMutation("whitelist", "add"); - }); - } - if ( - this.adminWhitelistInput && - this.adminWhitelistInput.dataset.bound !== "true" - ) { - this.adminWhitelistInput.dataset.bound = "true"; - this.adminWhitelistInput.addEventListener("keydown", (event) => { - if (event.key === "Enter") { - event.preventDefault(); - this.handleAdminListMutation("whitelist", "add"); - } - }); - } + this.profileHistoryRenderer = + this.profileModalController.profileHistoryRenderer; - if ( - this.adminAddBlacklistBtn && - this.adminAddBlacklistBtn.dataset.bound !== "true" - ) { - this.adminAddBlacklistBtn.dataset.bound = "true"; - this.adminAddBlacklistBtn.addEventListener("click", () => { - this.handleAdminListMutation("blacklist", "add"); - }); - } - if ( - this.adminBlacklistInput && - this.adminBlacklistInput.dataset.bound !== "true" - ) { - this.adminBlacklistInput.dataset.bound = "true"; - this.adminBlacklistInput.addEventListener("keydown", (event) => { - if (event.key === "Enter") { - event.preventDefault(); - this.handleAdminListMutation("blacklist", "add"); - } - }); - } + this.adminModeratorsSection = + this.profileModalController.adminModeratorsSection; + this.adminModeratorsEmpty = + this.profileModalController.adminModeratorsEmpty; + this.adminModeratorList = + this.profileModalController.adminModeratorList; + this.adminModeratorInput = + this.profileModalController.adminModeratorInput; + this.adminAddModeratorBtn = + this.profileModalController.adminAddModeratorButton; - this.selectProfilePane("account"); - this.populateProfileRelays(); - this.populateBlockedList(); - this.refreshWalletPaneState(); - await this.refreshAdminPaneState(); + this.adminWhitelistSection = + this.profileModalController.adminWhitelistSection; + this.adminWhitelistEmpty = + this.profileModalController.adminWhitelistEmpty; + this.adminWhitelistList = + this.profileModalController.adminWhitelistList; + this.adminWhitelistInput = + this.profileModalController.adminWhitelistInput; + this.adminAddWhitelistBtn = + this.profileModalController.adminAddWhitelistButton; - this.renderSavedProfiles(); + this.adminBlacklistSection = + this.profileModalController.adminBlacklistSection; + this.adminBlacklistEmpty = + this.profileModalController.adminBlacklistEmpty; + this.adminBlacklistList = + this.profileModalController.adminBlacklistList; + this.adminBlacklistInput = + this.profileModalController.adminBlacklistInput; + this.adminAddBlacklistBtn = + this.profileModalController.adminAddBlacklistButton; - console.log("Profile modal initialization successful"); return true; } catch (error) { console.error("initProfileModal failed:", error); - // Not critical if missing return false; } } diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js index a099c6e9..6e3006d8 100644 --- a/js/ui/profileModalController.js +++ b/js/ui/profileModalController.js @@ -446,10 +446,17 @@ export class ProfileModalController { } const html = await response.text(); - const wrapper = document.createElement("div"); - wrapper.innerHTML = html; - this.removeTrackingScripts(wrapper); - this.modalContainer.appendChild(wrapper); + const template = document.createElement("template"); + template.innerHTML = html; + this.removeTrackingScripts(template.content); + + const modalRoot = template.content.querySelector("#profileModal"); + if (!(modalRoot instanceof HTMLElement)) { + throw new Error("profile modal markup missing expected #profileModal root"); + } + + this.modalContainer.appendChild(template.content); + this.profileModal = modalRoot; this.cacheDomReferences(); this.registerEventListeners(); @@ -1166,38 +1173,55 @@ export class ProfileModalController { skipPublishIfUnchanged = true, unchangedMessage = null, } = {}) { + const operationContext = { + ok: false, + changed: false, + reason: null, + error: null, + publishResult: null, + operationResult: null, + }; + const activePubkey = this.normalizeHexPubkey(this.getActivePubkey()); if (!activePubkey) { this.showError("Please login to manage your relays."); - return; + operationContext.reason = "no-active-pubkey"; + return operationContext; } if (typeof operation !== "function") { - return; + operationContext.reason = "invalid-operation"; + return operationContext; } const previous = this.services.relayManager.snapshot(); let result; try { result = operation(); + operationContext.operationResult = result; } catch (error) { const message = error && typeof error.message === "string" && error.message.trim() ? error.message.trim() : "Failed to update relay preferences."; + operationContext.reason = "operation-error"; + operationContext.error = error; this.showError(message); - return; + return operationContext; } const changed = !!result?.changed; + operationContext.changed = changed; if (!changed && skipPublishIfUnchanged) { + const reason = result?.reason || "unchanged"; + operationContext.reason = reason; if (result?.reason === "duplicate") { this.showSuccess("Relay is already configured."); } else if (typeof unchangedMessage === "string" && unchangedMessage) { this.showSuccess(unchangedMessage); } this.populateProfileRelays(); - return; + return operationContext; } this.populateProfileRelays(); @@ -1209,9 +1233,12 @@ export class ProfileModalController { if (!publishResult?.ok) { throw new Error("No relays accepted the update."); } + operationContext.ok = true; + operationContext.publishResult = publishResult; if (successMessage) { this.showSuccess(successMessage); } + return operationContext; } catch (error) { this.services.relayManager.setEntries(previous, { allowEmpty: false }); this.populateProfileRelays(); @@ -1219,28 +1246,37 @@ export class ProfileModalController { error && typeof error.message === "string" && error.message.trim() ? error.message.trim() : "Failed to publish relay configuration. Please try again."; + operationContext.reason = "publish-failed"; + operationContext.error = error; this.showError(message); + return operationContext; } } async handleAddRelay() { - const activePubkey = this.normalizeHexPubkey(this.getActivePubkey()); - if (!activePubkey) { - this.showError("Please login to manage your relays."); - return; - } - const rawValue = typeof this.profileRelayInput?.value === "string" ? this.profileRelayInput.value : ""; const trimmed = rawValue.trim(); + + const context = { + input: this.profileRelayInput, + rawValue, + url: trimmed, + result: null, + success: false, + reason: null, + }; + if (!trimmed) { this.showError("Enter a relay URL to add."); - return; + context.reason = "empty"; + this.callbacks.onAddRelay(context, this); + return context; } - await this.handleRelayOperation( + const operationResult = await this.handleRelayOperation( () => this.services.relayManager.addRelay(trimmed), { successMessage: "Relay saved.", @@ -1252,22 +1288,31 @@ export class ProfileModalController { this.profileRelayInput.value = ""; } - this.callbacks.onAddRelay(this.profileRelayInput, this); + context.result = operationResult; + context.success = !!operationResult?.ok; + context.reason = operationResult?.reason || null; + + this.callbacks.onAddRelay(context, this); + return context; } async handleRestoreRelays() { - const activePubkey = this.normalizeHexPubkey(this.getActivePubkey()); - if (!activePubkey) { - this.showError("Please login to manage your relays."); - return; - } + const context = { + confirmed: false, + result: null, + success: false, + reason: null, + }; const confirmed = window.confirm("Restore the recommended relay defaults?"); + context.confirmed = confirmed; if (!confirmed) { - return; + context.reason = "cancelled"; + this.callbacks.onRestoreRelays(context, this); + return context; } - await this.handleRelayOperation( + const operationResult = await this.handleRelayOperation( () => this.services.relayManager.restoreDefaults(), { successMessage: "Relay defaults restored.", @@ -1275,7 +1320,12 @@ export class ProfileModalController { }, ); - this.callbacks.onRestoreRelays(this); + context.result = operationResult; + context.success = !!operationResult?.ok; + context.reason = operationResult?.reason || null; + + this.callbacks.onRestoreRelays(context, this); + return context; } async handleRelayModeToggle(url) { @@ -1428,22 +1478,38 @@ export class ProfileModalController { } async handleAddBlockedCreator() { - if (!this.profileBlockedInput) { - return; - } + const input = this.profileBlockedInput || null; + const rawValue = typeof input?.value === "string" ? input.value : ""; + const trimmed = rawValue.trim(); - const rawValue = this.profileBlockedInput.value; - const trimmed = typeof rawValue === "string" ? rawValue.trim() : ""; + const context = { + input, + rawValue, + value: trimmed, + success: false, + reason: null, + error: null, + }; + + if (!input) { + context.reason = "missing-input"; + this.callbacks.onAddBlocked(context, this); + return context; + } if (!trimmed) { this.showError("Enter an npub to block."); - return; + context.reason = "empty"; + this.callbacks.onAddBlocked(context, this); + return context; } const activePubkey = this.normalizeHexPubkey(this.getActivePubkey()); if (!activePubkey) { this.showError("Please login to manage your block list."); - return; + context.reason = "no-active-pubkey"; + this.callbacks.onAddBlocked(context, this); + return context; } const actorHex = activePubkey; @@ -1453,30 +1519,41 @@ export class ProfileModalController { targetHex = this.safeDecodeNpub(trimmed) || ""; if (!targetHex) { this.showError("Invalid npub. Please double-check and try again."); - return; + context.reason = "invalid-npub"; + this.callbacks.onAddBlocked(context, this); + return context; } } else if (/^[0-9a-f]{64}$/i.test(trimmed)) { targetHex = trimmed.toLowerCase(); } else { this.showError("Enter a valid npub or hex pubkey."); - return; + context.reason = "invalid-value"; + this.callbacks.onAddBlocked(context, this); + return context; } if (targetHex === actorHex) { this.showError("You cannot block yourself."); - return; + context.reason = "self"; + this.callbacks.onAddBlocked(context, this); + return context; } + context.targetHex = targetHex; + try { await this.services.userBlocks.ensureLoaded(actorHex); if (this.services.userBlocks.isBlocked(targetHex)) { this.showSuccess("You already blocked this creator."); + context.reason = "already-blocked"; } else { await this.services.userBlocks.addBlock(targetHex, actorHex); this.showSuccess( "Creator blocked. You won't see their videos anymore.", ); + context.success = true; + context.reason = "blocked"; } this.profileBlockedInput.value = ""; @@ -1484,6 +1561,8 @@ export class ProfileModalController { await this.services.loadVideos(); } catch (error) { console.error("Failed to add creator to personal block list:", error); + context.error = error; + context.reason = error?.code || "service-error"; const message = error?.code === "nip04-missing" ? "Your Nostr extension must support NIP-04 to manage private lists." @@ -1491,7 +1570,8 @@ export class ProfileModalController { this.showError(message); } - this.callbacks.onAddBlocked(this.profileBlockedInput, this); + this.callbacks.onAddBlocked(context, this); + return context; } async handleRemoveBlockedCreator(candidate) { @@ -1751,28 +1831,47 @@ export class ProfileModalController { } async handleWalletSave() { + const { uri, defaultZap, error } = this.getWalletFormValues(); + const context = { + uri, + defaultZap: defaultZap ?? null, + sanitizedUri: null, + success: false, + reason: null, + error: error || null, + status: null, + variant: null, + }; + if (this.isWalletBusy()) { - return; + context.reason = "busy"; + this.callbacks.onWalletSave(context, this); + return context; } - const { uri, defaultZap, error } = this.getWalletFormValues(); if (error) { this.updateWalletStatus(error, "error"); this.showError(error); + context.reason = "invalid-default-zap"; if (this.walletDefaultZapInput instanceof HTMLElement) { this.walletDefaultZapInput.focus(); } - return; + this.callbacks.onWalletSave(context, this); + return context; } const { valid, sanitized, message } = this.validateWalletUri(uri); + context.sanitizedUri = sanitized; if (!valid) { this.updateWalletStatus(message, "error"); this.showError(message); + context.reason = "invalid-uri"; + context.error = message; if (this.walletUriInput instanceof HTMLElement) { this.walletUriInput.focus(); } - return; + this.callbacks.onWalletSave(context, this); + return context; } const normalizedActive = this.normalizeHexPubkey(this.getActivePubkey()); @@ -1780,7 +1879,10 @@ export class ProfileModalController { const loginMessage = "Sign in to save wallet settings."; this.updateWalletStatus(loginMessage, "error"); this.showError(loginMessage); - return; + context.reason = "no-active-pubkey"; + context.error = loginMessage; + this.callbacks.onWalletSave(context, this); + return context; } this.setWalletPaneBusy(true); @@ -1796,11 +1898,14 @@ export class ProfileModalController { finalStatus = "Wallet settings saved."; finalVariant = "success"; this.showSuccess("Wallet settings saved."); + context.reason = "saved"; } else { finalStatus = "Wallet connection removed."; finalVariant = "info"; this.showStatus("Wallet connection removed."); + context.reason = "cleared"; } + context.success = true; } catch (error) { const fallbackMessage = "Failed to save wallet settings."; const detail = @@ -1809,6 +1914,8 @@ export class ProfileModalController { : fallbackMessage; finalStatus = detail; finalVariant = "error"; + context.error = detail; + context.reason = error?.code || "service-error"; this.showError(detail); } finally { this.setWalletPaneBusy(false); @@ -1816,34 +1923,59 @@ export class ProfileModalController { if (finalStatus) { this.updateWalletStatus(finalStatus, finalVariant); } + context.status = finalStatus; + context.variant = finalVariant; + this.callbacks.onWalletSave(context, this); } + + return context; } async handleWalletTest() { + const { uri, defaultZap, error } = this.getWalletFormValues(); + const context = { + uri, + defaultZap: defaultZap ?? null, + sanitizedUri: null, + success: false, + reason: null, + error: error || null, + status: null, + variant: null, + result: null, + }; + if (this.isWalletBusy()) { - return; + context.reason = "busy"; + this.callbacks.onWalletTest(context, this); + return context.result; } - const { uri, defaultZap, error } = this.getWalletFormValues(); if (error) { this.updateWalletStatus(error, "error"); this.showError(error); + context.reason = "invalid-default-zap"; if (this.walletDefaultZapInput instanceof HTMLElement) { this.walletDefaultZapInput.focus(); } - return; + this.callbacks.onWalletTest(context, this); + return context.result; } const { valid, sanitized, message } = this.validateWalletUri(uri, { requireValue: true, }); + context.sanitizedUri = sanitized; if (!valid) { this.updateWalletStatus(message, "error"); this.showError(message); + context.reason = "invalid-uri"; + context.error = message; if (this.walletUriInput instanceof HTMLElement) { this.walletUriInput.focus(); } - return; + this.callbacks.onWalletTest(context, this); + return context.result; } const normalizedActive = this.normalizeHexPubkey(this.getActivePubkey()); @@ -1851,7 +1983,10 @@ export class ProfileModalController { const loginMessage = "Sign in to test your wallet connection."; this.updateWalletStatus(loginMessage, "error"); this.showError(loginMessage); - return; + context.reason = "no-active-pubkey"; + context.error = loginMessage; + this.callbacks.onWalletTest(context, this); + return context.result; } this.setWalletPaneBusy(true); @@ -1865,12 +2000,14 @@ export class ProfileModalController { finalStatus = "Wallet connection confirmed."; finalVariant = "success"; this.showSuccess("Wallet connection confirmed."); + context.result = result; + context.success = true; + context.reason = "tested"; const currentSettings = this.services.getActiveNwcSettings(); if (currentSettings.nwcUri === sanitized) { await this.services.updateActiveNwcSettings({ lastChecked: Date.now() }); } - return result; } catch (error) { const fallbackMessage = "Failed to reach wallet."; const detail = @@ -1879,20 +2016,36 @@ export class ProfileModalController { : fallbackMessage; finalStatus = detail; finalVariant = "error"; + context.error = detail; + context.reason = error?.code || "service-error"; this.showError(detail); - return null; } finally { this.setWalletPaneBusy(false); this.refreshWalletPaneState(); if (finalStatus) { this.updateWalletStatus(finalStatus, finalVariant); } + context.status = finalStatus; + context.variant = finalVariant; + this.callbacks.onWalletTest(context, this); } + + return context.result; } async handleWalletDisconnect() { + const context = { + success: false, + reason: null, + error: null, + status: null, + variant: null, + }; + if (this.isWalletBusy()) { - return; + context.reason = "busy"; + this.callbacks.onWalletDisconnect(context, this); + return context; } const normalizedActive = this.normalizeHexPubkey(this.getActivePubkey()); @@ -1900,7 +2053,10 @@ export class ProfileModalController { const loginMessage = "Sign in to disconnect your wallet."; this.updateWalletStatus(loginMessage, "error"); this.showError(loginMessage); - return; + context.reason = "no-active-pubkey"; + context.error = loginMessage; + this.callbacks.onWalletDisconnect(context, this); + return context; } this.setWalletPaneBusy(true); @@ -1912,6 +2068,8 @@ export class ProfileModalController { ); finalStatus = "Wallet disconnected."; this.showStatus("Wallet disconnected."); + context.success = true; + context.reason = "disconnected"; } catch (error) { const fallbackMessage = "Failed to disconnect wallet."; const detail = @@ -1920,6 +2078,8 @@ export class ProfileModalController { : fallbackMessage; finalStatus = detail; finalVariant = "error"; + context.error = detail; + context.reason = error?.code || "service-error"; this.showError(detail); } finally { this.setWalletPaneBusy(false); @@ -1927,7 +2087,12 @@ export class ProfileModalController { if (finalStatus) { this.updateWalletStatus(finalStatus, finalVariant); } + context.status = finalStatus; + context.variant = finalVariant; + this.callbacks.onWalletDisconnect(context, this); } + + return context; } storeAdminEmptyMessages() { @@ -2211,6 +2376,20 @@ export class ProfileModalController { } async handleAddModerator() { + const input = this.adminModeratorInput || null; + const rawValue = typeof input?.value === "string" ? input.value : ""; + const trimmed = rawValue.trim(); + const context = { + input, + rawValue, + value: trimmed, + actorNpub: null, + success: false, + reason: null, + error: null, + result: null, + }; + let preloadError = null; try { await this.services.accessControl.ensureReady(); @@ -2221,18 +2400,25 @@ export class ProfileModalController { if (preloadError) { this.showError(this.describeAdminError(preloadError.code || "storage-error")); - return; + context.reason = preloadError.code || "storage-error"; + context.error = preloadError; + this.callbacks.onAdminAddModerator(context, this); + return context; } const actorNpub = this.ensureAdminActor(true); - if (!actorNpub || !this.adminModeratorInput) { - return; + context.actorNpub = actorNpub; + if (!actorNpub || !input) { + context.reason = actorNpub ? "missing-input" : "unauthorized"; + this.callbacks.onAdminAddModerator(context, this); + return context; } - const value = this.adminModeratorInput.value.trim(); - if (!value) { + if (!trimmed) { this.showError("Enter an npub to add as a moderator."); - return; + context.reason = "empty"; + this.callbacks.onAdminAddModerator(context, this); + return context; } if (this.adminAddModeratorButton) { @@ -2241,24 +2427,51 @@ export class ProfileModalController { } try { - const result = await this.services.accessControl.addModerator(actorNpub, value); + const result = await this.services.accessControl.addModerator( + actorNpub, + trimmed, + ); + context.result = result; if (!result.ok) { this.showError(this.describeAdminError(result.error)); - return; + context.reason = result.error || "service-error"; + return context; } this.adminModeratorInput.value = ""; this.showSuccess("Moderator added successfully."); await this.services.onAccessControlUpdated(); + context.success = true; + context.reason = "added"; } finally { if (this.adminAddModeratorButton) { this.adminAddModeratorButton.disabled = false; this.adminAddModeratorButton.removeAttribute("aria-busy"); } + this.callbacks.onAdminAddModerator(context, this); } + + return context; } async handleRemoveModerator(npub, button) { + const context = { + npub, + button, + actorNpub: null, + success: false, + reason: null, + error: null, + result: null, + }; + + const releaseButton = () => { + if (button instanceof HTMLElement) { + button.disabled = false; + button.removeAttribute("aria-busy"); + } + }; + let preloadError = null; try { await this.services.accessControl.ensureReady(); @@ -2269,40 +2482,97 @@ export class ProfileModalController { if (preloadError) { this.showError(this.describeAdminError(preloadError.code || "storage-error")); - if (button instanceof HTMLElement) { - button.disabled = false; - button.removeAttribute("aria-busy"); - } - return; + context.reason = preloadError.code || "storage-error"; + context.error = preloadError; + releaseButton(); + this.callbacks.onAdminRemoveModerator(context, this); + return context; } const actorNpub = this.ensureAdminActor(true); + context.actorNpub = actorNpub; if (!actorNpub) { - if (button instanceof HTMLElement) { - button.disabled = false; - button.removeAttribute("aria-busy"); - } - return; + context.reason = "unauthorized"; + releaseButton(); + this.callbacks.onAdminRemoveModerator(context, this); + return context; } const result = await this.services.accessControl.removeModerator( actorNpub, npub, ); + context.result = result; if (!result.ok) { this.showError(this.describeAdminError(result.error)); - if (button instanceof HTMLElement) { - button.disabled = false; - button.removeAttribute("aria-busy"); - } - return; + context.reason = result.error || "service-error"; + releaseButton(); + this.callbacks.onAdminRemoveModerator(context, this); + return context; } this.showSuccess("Moderator removed."); await this.services.onAccessControlUpdated(); + context.success = true; + context.reason = "removed"; + + releaseButton(); + this.callbacks.onAdminRemoveModerator(context, this); + return context; } async handleAdminListMutation(listType, action, explicitNpub = null, sourceButton = null) { + const isWhitelist = listType === "whitelist"; + const input = isWhitelist ? this.adminWhitelistInput : this.adminBlacklistInput; + const addButton = isWhitelist ? this.adminAddWhitelistButton : this.adminAddBlacklistButton; + const isAdd = action === "add"; + let buttonToToggle = sourceButton || (isAdd ? addButton : null); + + const context = { + listType, + action, + explicitNpub, + sourceButton, + actorNpub: null, + targetNpub: null, + success: false, + reason: null, + error: null, + result: null, + notificationResult: null, + notificationError: null, + }; + + const callbackMap = { + whitelist: { + add: this.callbacks.onAdminAddWhitelist, + remove: this.callbacks.onAdminRemoveWhitelist, + }, + blacklist: { + add: this.callbacks.onAdminAddBlacklist, + remove: this.callbacks.onAdminRemoveBlacklist, + }, + }; + + const adminCallback = callbackMap[listType]?.[action] || noop; + + const setBusy = (element, busy) => { + if (!(element instanceof HTMLElement)) { + return; + } + element.disabled = !!busy; + if (busy) { + element.setAttribute("aria-busy", "true"); + } else { + element.removeAttribute("aria-busy"); + } + }; + + const finalize = () => { + setBusy(buttonToToggle, false); + adminCallback(context, this); + }; + let preloadError = null; try { await this.services.accessControl.ensureReady(); @@ -2313,46 +2583,35 @@ export class ProfileModalController { if (preloadError) { this.showError(this.describeAdminError(preloadError.code || "storage-error")); - if (sourceButton instanceof HTMLElement) { - sourceButton.disabled = false; - sourceButton.removeAttribute("aria-busy"); - } - return; + context.reason = preloadError.code || "storage-error"; + context.error = preloadError; + finalize(); + return context; } const actorNpub = this.ensureAdminActor(false); + context.actorNpub = actorNpub; if (!actorNpub) { - if (sourceButton instanceof HTMLElement) { - sourceButton.disabled = false; - sourceButton.removeAttribute("aria-busy"); - } - return; + context.reason = "unauthorized"; + finalize(); + return context; } - const isWhitelist = listType === "whitelist"; - const input = isWhitelist ? this.adminWhitelistInput : this.adminBlacklistInput; - const addButton = isWhitelist ? this.adminAddWhitelistButton : this.adminAddBlacklistButton; - const isAdd = action === "add"; - let target = typeof explicitNpub === "string" ? explicitNpub.trim() : ""; if (!target && input instanceof HTMLInputElement) { target = input.value.trim(); } + context.targetNpub = target; if (isAdd && !target) { this.showError("Enter an npub before adding it to the list."); - if (sourceButton instanceof HTMLElement) { - sourceButton.disabled = false; - sourceButton.removeAttribute("aria-busy"); - } - return; + context.reason = "empty"; + finalize(); + return context; } - const buttonToToggle = sourceButton || (isAdd ? addButton : null); - if (buttonToToggle instanceof HTMLElement) { - buttonToToggle.disabled = true; - buttonToToggle.setAttribute("aria-busy", "true"); - } + buttonToToggle = buttonToToggle || (isAdd ? addButton : null); + setBusy(buttonToToggle, true); let result; if (isWhitelist) { @@ -2365,13 +2624,13 @@ export class ProfileModalController { : await this.services.accessControl.removeFromBlacklist(actorNpub, target); } + context.result = result; + if (!result.ok) { this.showError(this.describeAdminError(result.error)); - if (buttonToToggle instanceof HTMLElement) { - buttonToToggle.disabled = false; - buttonToToggle.removeAttribute("aria-busy"); - } - return; + context.reason = result.error || "service-error"; + finalize(); + return context; } if (isAdd && input instanceof HTMLInputElement) { @@ -2388,10 +2647,8 @@ export class ProfileModalController { this.showSuccess(successMessage); await this.services.onAccessControlUpdated(); - if (buttonToToggle instanceof HTMLElement) { - buttonToToggle.disabled = false; - buttonToToggle.removeAttribute("aria-busy"); - } + context.success = true; + context.reason = isAdd ? "added" : "removed"; if (isAdd) { try { @@ -2400,6 +2657,7 @@ export class ProfileModalController { actorNpub, targetNpub: target, }); + context.notificationResult = notifyResult; if (!notifyResult?.ok) { const errorMessage = this.describeNotificationError(notifyResult?.error); if (errorMessage) { @@ -2413,6 +2671,7 @@ export class ProfileModalController { } } } catch (error) { + context.notificationError = error; console.error("Failed to send list notification DM:", error); if (isDevMode) { console.warn( @@ -2422,6 +2681,9 @@ export class ProfileModalController { } } } + + finalize(); + return context; } describeAdminError(code) {