diff --git a/js/app.js b/js/app.js index 6f95ad60..580df861 100644 --- a/js/app.js +++ b/js/app.js @@ -1338,6 +1338,21 @@ class Application { } } + dispatchAuthChange(detail = {}) { + if (typeof window === "undefined" || typeof window.dispatchEvent !== "function") { + return false; + } + + try { + const payload = { ...(typeof detail === "object" && detail ? detail : {}) }; + window.dispatchEvent(new CustomEvent("bitvid:auth-changed", { detail: payload })); + return true; + } catch (error) { + devLogger.warn("[Application] Failed to dispatch auth change event:", error); + return false; + } + } + normalizeModalTrigger(candidate) { if (!candidate) { return null; @@ -3107,6 +3122,23 @@ class Application { identityChanged: Boolean(detail?.identityChanged), }; + if (this.zapController) { + try { + this.zapController.setVisibility( + Boolean(this.currentVideo?.lightningAddress), + ); + } catch (error) { + devLogger.warn("[Application] Failed to refresh zap visibility after login:", error); + } + } + + this.dispatchAuthChange({ + status: "login", + loggedIn: true, + pubkey: loginContext.pubkey || null, + previousPubkey: loginContext.previousPubkey || null, + }); + const nwcPromise = Promise.resolve() .then(() => this.nwcSettingsService.onLogin(loginContext)) .catch((error) => { @@ -3198,6 +3230,51 @@ class Application { this.applyLoggedOutUiState(); + if (this.videoModal?.closeZapDialog) { + try { + this.videoModal.closeZapDialog({ silent: true, restoreFocus: false }); + } catch (error) { + devLogger.warn("Failed to close zap dialog during logout:", error); + } + } + + if (this.zapController) { + try { + this.zapController.resetState(); + this.zapController.setVisibility(Boolean(this.currentVideo?.lightningAddress)); + } catch (error) { + devLogger.warn("Failed to reset zap controller during logout:", error); + } + } + + if (typeof this.nostrService?.clearVideoSubscription === "function") { + try { + this.nostrService.clearVideoSubscription(); + } catch (error) { + devLogger.warn("Failed to clear video subscription during logout:", error); + } + } + + if (typeof this.nostrService?.resetVideosCache === "function") { + try { + this.nostrService.resetVideosCache(); + } catch (error) { + devLogger.warn("Failed to reset cached videos during logout:", error); + } + } + + await this.renderVideoList({ + videos: [], + metadata: { reason: "auth:logout" }, + }); + + this.dispatchAuthChange({ + status: "logout", + loggedIn: false, + pubkey: detail?.pubkey || null, + previousPubkey: detail?.previousPubkey || null, + }); + try { await this.loadVideos(true); } catch (error) { diff --git a/js/channelProfile.js b/js/channelProfile.js index 92a1461e..4f422611 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -3745,6 +3745,51 @@ window.addEventListener("bitvid:access-control-updated", () => { }); }); +window.addEventListener("bitvid:auth-changed", (event) => { + const hashParams = new URLSearchParams(window.location.hash.slice(1)); + if (hashParams.get("view") !== "channel-profile") { + return; + } + + if (!currentChannelHex) { + return; + } + + const activeNpub = hashParams.get("npub"); + if (currentChannelNpub && activeNpub && activeNpub !== currentChannelNpub) { + return; + } + + setChannelZapVisibility(Boolean(currentChannelLightningAddress)); + syncChannelShareButtonState(); + + const detail = event?.detail; + if (!detail || typeof detail !== "object") { + return; + } + + if (detail.status === "logout") { + zapInFlight = false; + zapPopoverOpenPromise = null; + zapShouldFocusOnOpen = false; + resetZapRetryState(); + setZapStatus("", "neutral"); + clearZapReceipts(); + const amountInput = getZapAmountInput(); + if (amountInput) { + amountInput.value = ""; + } + return; + } + + if (detail.status === "login") { + zapInFlight = false; + resetZapRetryState(); + setZapStatus("", "neutral"); + return; + } +}); + /** * Keep only the newest version of each video root. */ diff --git a/js/services/nostrService.js b/js/services/nostrService.js index ddfab67d..09a5b962 100644 --- a/js/services/nostrService.js +++ b/js/services/nostrService.js @@ -218,6 +218,13 @@ export class NostrService { this.emit("videos:cache", { size: map.size }); } + resetVideosCache() { + const map = new Map(); + this.videosMap = map; + setStoredVideosMap(map); + this.emit("videos:cache", { size: 0 }); + } + shouldIncludeVideo(video, { blacklistedEventIds = new Set(), isAuthorBlocked = () => false, diff --git a/js/ui/components/VideoModal.js b/js/ui/components/VideoModal.js index b536062a..c5e919f3 100644 --- a/js/ui/components/VideoModal.js +++ b/js/ui/components/VideoModal.js @@ -79,6 +79,8 @@ export class VideoModal { this.modalZapPending = false; this.modalZapRequiresLogin = false; this.modalZapPopover = null; + this.modalZapOpenPromise = null; + this.modalZapPendingToggle = null; this.modalMorePopover = null; this.modalMoreMenuPanel = null; @@ -238,6 +240,8 @@ export class VideoModal { this.modalMorePopover.destroy(); } this.modalZapPopover = null; + this.modalZapOpenPromise = null; + this.modalZapPendingToggle = null; this.modalMorePopover = null; this.modalMoreMenuPanel = null; @@ -428,6 +432,11 @@ export class VideoModal { ? this.modalZapPopover.isOpen() : this.modalZapDialogOpen === true; + if (this.modalZapOpenPromise) { + this.modalZapPendingToggle = "close"; + return; + } + if (popoverIsOpen) { if (this.modalZapPending) { return; @@ -437,6 +446,7 @@ export class VideoModal { return; } + this.modalZapPendingToggle = null; this.openZapDialog(); this.dispatch("zap:open", { video: this.activeVideo }); }); @@ -587,6 +597,8 @@ export class VideoModal { const wasOpen = popover.isOpen?.() === true; const result = originalClose(rest); if (wasOpen && result) { + this.modalZapPendingToggle = null; + this.modalZapOpenPromise = null; this.modalZapDialog.dataset.state = "closed"; this.modalZapDialog.setAttribute("aria-hidden", "true"); this.modalZapDialog.hidden = true; @@ -1042,8 +1054,12 @@ export class VideoModal { } async openZapDialog() { - if (this.modalZapPopover?.open) { - try { + if (this.modalZapOpenPromise) { + return this.modalZapOpenPromise; + } + + const runOpen = async () => { + if (this.modalZapPopover?.open) { const opened = await this.modalZapPopover.open(); if (opened) { this.modalZapDialogOpen = true; @@ -1054,27 +1070,43 @@ export class VideoModal { } this.focusZapAmount(); } - return; - } catch (error) { - this.log("[VideoModal] Failed to open zap popover", error); + return opened; } - } - if (!this.modalZapDialog) { - return; - } + if (!this.modalZapDialog) { + return false; + } - this.modalZapDialog.hidden = false; - this.modalZapDialog.dataset.state = "open"; - this.modalZapDialog.setAttribute("aria-hidden", "false"); - this.modalZapDialogOpen = true; - if (this.modalZapBtn) { - this.modalZapBtn.setAttribute("aria-expanded", "true"); - } - this.focusZapAmount(); + this.modalZapDialog.hidden = false; + this.modalZapDialog.dataset.state = "open"; + this.modalZapDialog.setAttribute("aria-hidden", "false"); + this.modalZapDialogOpen = true; + if (this.modalZapBtn) { + this.modalZapBtn.setAttribute("aria-expanded", "true"); + } + this.focusZapAmount(); + return true; + }; + + const promise = runOpen().catch((error) => { + this.log("[VideoModal] Failed to open zap popover", error); + return false; + }); + + this.modalZapOpenPromise = promise.finally(() => { + const shouldClose = this.modalZapPendingToggle === "close"; + this.modalZapOpenPromise = null; + this.modalZapPendingToggle = null; + if (shouldClose && !this.modalZapPending) { + this.closeZapDialog(); + } + }); + + return this.modalZapOpenPromise; } closeZapDialog({ silent = false, restoreFocus } = {}) { + this.modalZapPendingToggle = null; if (this.modalZapPopover?.close) { const options = { silent }; if (restoreFocus !== undefined) {