From a0cc19c16ff62195819f69971a97f59fb578250e Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 18 Oct 2025 11:54:22 -0400 Subject: [PATCH 1/8] Improve zap popover responsiveness --- js/channelProfile.js | 46 +++++++++++++++++++++++++++++++++- js/ui/overlay/popoverEngine.js | 5 ++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/js/channelProfile.js b/js/channelProfile.js index 08f13623..a2825b87 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -936,7 +936,26 @@ function isZapControlsOpen() { function openZapControls({ focus = false } = {}) { const zapButton = getChannelZapButton(); - if (!zapButton || !zapPopover) { + if (!zapButton) { + return false; + } + + if (!zapPopover || zapPopoverTrigger !== zapButton) { + const initialized = setupZapButton({ force: true }); + if (!initialized || !zapPopover || zapPopoverTrigger !== zapButton) { + return false; + } + } + + if (typeof zapPopover?.preload === "function") { + try { + zapPopover.preload(); + } catch (error) { + devLogger.warn("[zap] Failed to preload zap popover before opening", error); + } + } + + if (!zapPopover) { return false; } zapShouldFocusOnOpen = focus; @@ -2473,6 +2492,31 @@ function setupZapButton({ force = false } = {}) { zapPopover = popover; zapPopoverTrigger = zapButton; + const warmZapPopover = () => { + if (!zapPopover || zapPopoverTrigger !== zapButton) { + return; + } + if (typeof zapPopover.preload === "function") { + try { + zapPopover.preload(); + } catch (error) { + devLogger.warn("[zap] Failed to preload zap popover", error); + } + } + }; + + warmZapPopover(); + + if (zapButton.dataset.zapPopoverWarmBound !== "true") { + const handleWarm = () => { + warmZapPopover(); + }; + zapButton.addEventListener("pointerenter", handleWarm, { once: true }); + zapButton.addEventListener("pointerdown", handleWarm, { once: true }); + zapButton.addEventListener("focus", handleWarm, { once: true }); + zapButton.dataset.zapPopoverWarmBound = "true"; + } + if (zapButton.dataset.initialized !== "true") { zapButton.addEventListener("click", handleZapButtonClick); zapButton.dataset.initialized = "true"; diff --git a/js/ui/overlay/popoverEngine.js b/js/ui/overlay/popoverEngine.js index 138b7d9c..d818c0e4 100644 --- a/js/ui/overlay/popoverEngine.js +++ b/js/ui/overlay/popoverEngine.js @@ -671,6 +671,10 @@ export function createPopover(trigger, render, options = {}) { return panel; } + function preload() { + return Boolean(ensurePanel()); + } + async function updatePosition() { if (!anchor || !panel) { return; @@ -918,6 +922,7 @@ export function createPopover(trigger, render, options = {}) { update: updatePosition, isOpen: () => isOpen, getPanel: () => panel, + preload, }; return api; From 7356dc06af39eb3a717dac1f1a3b758859191c2f Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 18 Oct 2025 13:00:12 -0400 Subject: [PATCH 2/8] Stabilize zap popover opening --- js/channelProfile.js | 94 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 80 insertions(+), 14 deletions(-) diff --git a/js/channelProfile.js b/js/channelProfile.js index a2825b87..cec2ac2b 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -133,6 +133,7 @@ let zapControlsOpen = false; let zapPopover = null; let zapPopoverTrigger = null; let zapShouldFocusOnOpen = false; +let zapPopoverOpenPromise = null; let channelMenuPopover = null; let channelMenuOpen = false; let currentVideoLoadToken = 0; @@ -928,13 +929,16 @@ function focusZapAmountField() { } function isZapControlsOpen() { + if (zapPopoverOpenPromise) { + return true; + } if (typeof zapPopover?.isOpen === "function") { return zapPopover.isOpen(); } return zapControlsOpen; } -function openZapControls({ focus = false } = {}) { +async function openZapControls({ focus = false } = {}) { const zapButton = getChannelZapButton(); if (!zapButton) { return false; @@ -958,23 +962,64 @@ function openZapControls({ focus = false } = {}) { if (!zapPopover) { return false; } - zapShouldFocusOnOpen = focus; - const result = zapPopover.open(); - if (result && typeof result.then === "function") { - result.catch(() => {}).finally(() => { - zapShouldFocusOnOpen = false; - }); + if (typeof zapPopover.isOpen === "function" && zapPopover.isOpen()) { + if (focus) { + focusZapAmountField(); + } return true; } - zapShouldFocusOnOpen = false; - return Boolean(result); + + if (zapPopoverOpenPromise) { + try { + return await zapPopoverOpenPromise; + } catch (error) { + devLogger.warn("[zap] Pending zap popover open failed", error); + return false; + } + } + + zapShouldFocusOnOpen = focus; + + const callId = Symbol("zap-open-request"); + zapPopover.__zapOpenCallId = callId; + + const result = zapPopover.open(); + if (!result || typeof result.then !== "function") { + const didOpen = Boolean(result) && zapPopover?.__zapOpenCallId === callId; + if (!didOpen) { + zapShouldFocusOnOpen = false; + } + return didOpen; + } + + const pendingOpen = result + .then((value) => { + if (!value) { + return false; + } + return Boolean(zapPopover) && zapPopover.__zapOpenCallId === callId; + }) + .catch((error) => { + devLogger.warn("[zap] Failed to open zap popover", error); + return false; + }) + .finally(() => { + if (zapPopoverOpenPromise === pendingOpen) { + zapPopoverOpenPromise = null; + } + }); + + zapPopoverOpenPromise = pendingOpen; + return pendingOpen; } function closeZapControls({ focusButton = false } = {}) { const zapButton = getChannelZapButton(); zapShouldFocusOnOpen = false; + zapPopoverOpenPromise = null; if (zapPopover) { + zapPopover.__zapOpenCallId = Symbol("zap-open-cancel"); const result = zapPopover.close({ restoreFocus: focusButton }); if (!result && focusButton && zapButton && typeof zapButton.focus === "function") { try { @@ -1642,7 +1687,7 @@ async function executePendingRetry({ walletSettings }) { resetZapRetryState(); } -function handleZapButtonClick(event) { +async function handleZapButtonClick(event) { if (event) { event.preventDefault(); if (typeof event.stopPropagation === "function") { @@ -1661,7 +1706,11 @@ function handleZapButtonClick(event) { : isZapControlsOpen(); if (!popoverIsOpen) { - openZapControls({ focus: true }); + try { + await openZapControls({ focus: true }); + } catch (error) { + devLogger.warn("[zap] Failed to handle zap button open request", error); + } return; } @@ -1687,7 +1736,11 @@ async function handleZapSend(event) { } if (!isZapControlsOpen()) { - openZapControls({ focus: true }); + try { + await openZapControls({ focus: true }); + } catch (error) { + devLogger.warn("[zap] Failed to reopen zap controls before send", error); + } return; } @@ -2438,8 +2491,17 @@ function setupZapButton({ force = false } = {}) { const originalOpen = popover.open?.bind(popover); if (originalOpen) { popover.open = async (...args) => { + const callId = + typeof popover.__zapOpenCallId !== "undefined" + ? popover.__zapOpenCallId + : Symbol("zap-open-call"); + if (typeof popover.__zapOpenCallId === "undefined") { + popover.__zapOpenCallId = callId; + } + const result = await originalOpen(...args); - if (result) { + const isLatestCall = popover.__zapOpenCallId === callId; + if (result && isLatestCall) { zapControlsOpen = true; const controls = getZapControlsContainer(); if (controls) { @@ -2452,14 +2514,16 @@ function setupZapButton({ force = false } = {}) { focusZapAmountField(); } } + const didOpen = Boolean(result) && isLatestCall; zapShouldFocusOnOpen = false; - return result; + return didOpen; }; } const originalClose = popover.close?.bind(popover); if (originalClose) { popover.close = (options = {}) => { + popover.__zapOpenCallId = Symbol("zap-open-cancel"); const wasOpen = typeof popover.isOpen === "function" && popover.isOpen(); const result = originalClose(options); if (wasOpen && result) { @@ -2484,6 +2548,8 @@ function setupZapButton({ force = false } = {}) { if (zapPopover === popover) { zapPopover = null; zapPopoverTrigger = null; + zapPopoverOpenPromise = null; + delete popover.__zapOpenCallId; } cacheZapPanelElements(null); }; From d47aa1d4fa95c0de912b388fa8e59c1abe134622 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 18 Oct 2025 13:06:52 -0400 Subject: [PATCH 3/8] Debounce zap popover open while warm --- js/channelProfile.js | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/js/channelProfile.js b/js/channelProfile.js index cec2ac2b..7adcbd45 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -929,15 +929,16 @@ function focusZapAmountField() { } function isZapControlsOpen() { - if (zapPopoverOpenPromise) { - return true; - } if (typeof zapPopover?.isOpen === "function") { return zapPopover.isOpen(); } return zapControlsOpen; } +function isZapControlsOpening() { + return Boolean(zapPopoverOpenPromise) && !isZapControlsOpen(); +} + async function openZapControls({ focus = false } = {}) { const zapButton = getChannelZapButton(); if (!zapButton) { @@ -970,6 +971,9 @@ async function openZapControls({ focus = false } = {}) { } if (zapPopoverOpenPromise) { + if (focus) { + zapShouldFocusOnOpen = true; + } try { return await zapPopoverOpenPromise; } catch (error) { @@ -1700,12 +1704,14 @@ async function handleZapButtonClick(event) { return; } - const popoverIsOpen = - typeof zapPopover?.isOpen === "function" - ? zapPopover.isOpen() - : isZapControlsOpen(); + const opening = isZapControlsOpening(); + const popoverIsOpen = isZapControlsOpen(); if (!popoverIsOpen) { + if (opening) { + zapShouldFocusOnOpen = true; + return; + } try { await openZapControls({ focus: true }); } catch (error) { @@ -1714,6 +1720,10 @@ async function handleZapButtonClick(event) { return; } + if (opening) { + return; + } + if (zapInFlight) { return; } From f0b1b6582fb30699dd42bb2401cde8327ff3fa46 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 18 Oct 2025 13:21:02 -0400 Subject: [PATCH 4/8] Stabilize zap popover hydration and preload --- js/channelProfile.js | 198 +++++++++++++++++++++---------------------- 1 file changed, 97 insertions(+), 101 deletions(-) diff --git a/js/channelProfile.js b/js/channelProfile.js index 7adcbd45..f37c99ab 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -133,7 +133,7 @@ let zapControlsOpen = false; let zapPopover = null; let zapPopoverTrigger = null; let zapShouldFocusOnOpen = false; -let zapPopoverOpenPromise = null; +let zapPopoverOpening = false; let channelMenuPopover = null; let channelMenuOpen = false; let currentVideoLoadToken = 0; @@ -805,6 +805,8 @@ function cacheZapPanelElements(panel) { cachedZapWalletLink = null; cachedZapCloseBtn = null; cachedZapSendBtn = null; + zapPopoverOpening = false; + zapShouldFocusOnOpen = false; return; } @@ -935,95 +937,65 @@ function isZapControlsOpen() { return zapControlsOpen; } -function isZapControlsOpening() { - return Boolean(zapPopoverOpenPromise) && !isZapControlsOpen(); -} - -async function openZapControls({ focus = false } = {}) { +function openZapControls({ focus = false } = {}) { const zapButton = getChannelZapButton(); if (!zapButton) { + zapShouldFocusOnOpen = false; return false; } if (!zapPopover || zapPopoverTrigger !== zapButton) { const initialized = setupZapButton({ force: true }); if (!initialized || !zapPopover || zapPopoverTrigger !== zapButton) { + zapShouldFocusOnOpen = false; return false; } } - if (typeof zapPopover?.preload === "function") { - try { - zapPopover.preload(); - } catch (error) { - devLogger.warn("[zap] Failed to preload zap popover before opening", error); - } - } - - if (!zapPopover) { - return false; - } - if (typeof zapPopover.isOpen === "function" && zapPopover.isOpen()) { + if (zapPopoverOpening) { if (focus) { - focusZapAmountField(); + zapShouldFocusOnOpen = true; } return true; } - if (zapPopoverOpenPromise) { - if (focus) { - zapShouldFocusOnOpen = true; - } - try { - return await zapPopoverOpenPromise; - } catch (error) { - devLogger.warn("[zap] Pending zap popover open failed", error); - return false; - } - } - zapShouldFocusOnOpen = focus; + zapPopoverOpening = true; - const callId = Symbol("zap-open-request"); - zapPopover.__zapOpenCallId = callId; - - const result = zapPopover.open(); - if (!result || typeof result.then !== "function") { - const didOpen = Boolean(result) && zapPopover?.__zapOpenCallId === callId; - if (!didOpen) { - zapShouldFocusOnOpen = false; - } - return didOpen; + let result; + try { + result = zapPopover.open(); + } catch (error) { + zapPopoverOpening = false; + zapShouldFocusOnOpen = false; + devLogger.warn("[zap] Failed to open zap popover", error); + return false; } - const pendingOpen = result - .then((value) => { - if (!value) { + if (result && typeof result.then === "function") { + result + .catch((error) => { + devLogger.warn("[zap] Zap popover open rejected", error); return false; - } - return Boolean(zapPopover) && zapPopover.__zapOpenCallId === callId; - }) - .catch((error) => { - devLogger.warn("[zap] Failed to open zap popover", error); - return false; - }) - .finally(() => { - if (zapPopoverOpenPromise === pendingOpen) { - zapPopoverOpenPromise = null; - } - }); + }) + .finally(() => { + zapPopoverOpening = false; + zapShouldFocusOnOpen = false; + }); + return true; + } - zapPopoverOpenPromise = pendingOpen; - return pendingOpen; + zapPopoverOpening = false; + zapShouldFocusOnOpen = false; + return Boolean(result); } function closeZapControls({ focusButton = false } = {}) { const zapButton = getChannelZapButton(); zapShouldFocusOnOpen = false; - zapPopoverOpenPromise = null; + zapPopoverOpening = false; if (zapPopover) { - zapPopover.__zapOpenCallId = Symbol("zap-open-cancel"); const result = zapPopover.close({ restoreFocus: focusButton }); if (!result && focusButton && zapButton && typeof zapButton.focus === "function") { try { @@ -1691,7 +1663,7 @@ async function executePendingRetry({ walletSettings }) { resetZapRetryState(); } -async function handleZapButtonClick(event) { +function handleZapButtonClick(event) { if (event) { event.preventDefault(); if (typeof event.stopPropagation === "function") { @@ -1704,23 +1676,23 @@ async function handleZapButtonClick(event) { return; } - const opening = isZapControlsOpening(); - const popoverIsOpen = isZapControlsOpen(); + if (!zapPopover || zapPopoverTrigger !== zapButton) { + setupZapButton({ force: true }); + } - if (!popoverIsOpen) { - if (opening) { - zapShouldFocusOnOpen = true; - return; - } - try { - await openZapControls({ focus: true }); - } catch (error) { - devLogger.warn("[zap] Failed to handle zap button open request", error); - } + if (!zapPopover) { return; } - if (opening) { + if (zapPopoverOpening) { + zapShouldFocusOnOpen = true; + return; + } + + const popoverIsOpen = isZapControlsOpen(); + + if (!popoverIsOpen) { + openZapControls({ focus: true }); return; } @@ -1745,12 +1717,24 @@ async function handleZapSend(event) { return; } + if (!zapPopover || zapPopoverTrigger !== zapButton) { + setupZapButton({ force: true }); + } + if (!isZapControlsOpen()) { - try { - await openZapControls({ focus: true }); - } catch (error) { - devLogger.warn("[zap] Failed to reopen zap controls before send", error); + if (zapPopoverOpening) { + zapShouldFocusOnOpen = true; + return; } + openZapControls({ focus: true }); + return; + } + + if (zapPopoverOpening) { + return; + } + + if (!zapPopover) { return; } @@ -2378,6 +2362,9 @@ function setupZapButton({ force = false } = {}) { zapPopover.destroy(); zapPopover = null; zapPopoverTrigger = null; + zapPopoverOpening = false; + zapShouldFocusOnOpen = false; + delete zapButton.dataset.zapPopoverWarmBound; } const documentRef = @@ -2495,23 +2482,23 @@ function setupZapButton({ force = false } = {}) { }); if (!popover) { + zapPopoverOpening = false; + zapShouldFocusOnOpen = false; return false; } const originalOpen = popover.open?.bind(popover); if (originalOpen) { popover.open = async (...args) => { - const callId = - typeof popover.__zapOpenCallId !== "undefined" - ? popover.__zapOpenCallId - : Symbol("zap-open-call"); - if (typeof popover.__zapOpenCallId === "undefined") { - popover.__zapOpenCallId = callId; + let result; + try { + result = await originalOpen(...args); + } catch (error) { + zapPopoverOpening = false; + zapShouldFocusOnOpen = false; + throw error; } - - const result = await originalOpen(...args); - const isLatestCall = popover.__zapOpenCallId === callId; - if (result && isLatestCall) { + if (result) { zapControlsOpen = true; const controls = getZapControlsContainer(); if (controls) { @@ -2524,16 +2511,15 @@ function setupZapButton({ force = false } = {}) { focusZapAmountField(); } } - const didOpen = Boolean(result) && isLatestCall; zapShouldFocusOnOpen = false; - return didOpen; + zapPopoverOpening = false; + return result; }; } const originalClose = popover.close?.bind(popover); if (originalClose) { popover.close = (options = {}) => { - popover.__zapOpenCallId = Symbol("zap-open-cancel"); const wasOpen = typeof popover.isOpen === "function" && popover.isOpen(); const result = originalClose(options); if (wasOpen && result) { @@ -2547,6 +2533,7 @@ function setupZapButton({ force = false } = {}) { zapButton.setAttribute("aria-expanded", "false"); } zapShouldFocusOnOpen = false; + zapPopoverOpening = false; return result; }; } @@ -2558,9 +2545,9 @@ function setupZapButton({ force = false } = {}) { if (zapPopover === popover) { zapPopover = null; zapPopoverTrigger = null; - zapPopoverOpenPromise = null; - delete popover.__zapOpenCallId; } + zapPopoverOpening = false; + zapShouldFocusOnOpen = false; cacheZapPanelElements(null); }; } @@ -2569,19 +2556,28 @@ function setupZapButton({ force = false } = {}) { zapPopoverTrigger = zapButton; const warmZapPopover = () => { - if (!zapPopover || zapPopoverTrigger !== zapButton) { + if (!zapPopover || zapPopover !== popover) { return; } - if (typeof zapPopover.preload === "function") { - try { - zapPopover.preload(); - } catch (error) { - devLogger.warn("[zap] Failed to preload zap popover", error); - } + if (typeof popover.preload !== "function") { + return; + } + try { + popover.preload(); + } catch (error) { + devLogger.warn("[zap] Failed to preload zap controls", error); } }; - warmZapPopover(); + if (typeof popover.preload === "function") { + if (typeof window !== "undefined" && typeof window.requestIdleCallback === "function") { + window.requestIdleCallback(() => warmZapPopover(), { timeout: 250 }); + } else { + setTimeout(() => { + warmZapPopover(); + }, 0); + } + } if (zapButton.dataset.zapPopoverWarmBound !== "true") { const handleWarm = () => { From e02fcf370aa842361b1e9d72e37d8c8f30324385 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 18 Oct 2025 13:39:54 -0400 Subject: [PATCH 5/8] Stabilize zap popover open state and preserve profile data --- js/channelProfile.js | 81 +++++++++++++++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 20 deletions(-) diff --git a/js/channelProfile.js b/js/channelProfile.js index f37c99ab..edd15595 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -112,6 +112,7 @@ function logZapError(stage, details = {}, error) { userLogger.error("[zap] Channel zap failure", summary, error); } let currentChannelProfileEvent = null; +let currentChannelProfileSnapshot = null; let cachedZapButton = null; let cachedChannelShareButton = null; @@ -133,7 +134,7 @@ let zapControlsOpen = false; let zapPopover = null; let zapPopoverTrigger = null; let zapShouldFocusOnOpen = false; -let zapPopoverOpening = false; +let zapPopoverOpenPromise = null; let channelMenuPopover = null; let channelMenuOpen = false; let currentVideoLoadToken = 0; @@ -805,7 +806,7 @@ function cacheZapPanelElements(panel) { cachedZapWalletLink = null; cachedZapCloseBtn = null; cachedZapSendBtn = null; - zapPopoverOpening = false; + zapPopoverOpenPromise = null; zapShouldFocusOnOpen = false; return; } @@ -952,7 +953,7 @@ function openZapControls({ focus = false } = {}) { } } - if (zapPopoverOpening) { + if (zapPopoverOpenPromise) { if (focus) { zapShouldFocusOnOpen = true; } @@ -960,40 +961,40 @@ function openZapControls({ focus = false } = {}) { } zapShouldFocusOnOpen = focus; - zapPopoverOpening = true; let result; try { result = zapPopover.open(); } catch (error) { - zapPopoverOpening = false; zapShouldFocusOnOpen = false; + zapPopoverOpenPromise = null; devLogger.warn("[zap] Failed to open zap popover", error); return false; } if (result && typeof result.then === "function") { - result + const pending = result .catch((error) => { devLogger.warn("[zap] Zap popover open rejected", error); return false; }) .finally(() => { - zapPopoverOpening = false; + zapPopoverOpenPromise = null; zapShouldFocusOnOpen = false; }); + zapPopoverOpenPromise = pending; return true; } - zapPopoverOpening = false; zapShouldFocusOnOpen = false; + zapPopoverOpenPromise = null; return Boolean(result); } function closeZapControls({ focusButton = false } = {}) { const zapButton = getChannelZapButton(); zapShouldFocusOnOpen = false; - zapPopoverOpening = false; + zapPopoverOpenPromise = null; if (zapPopover) { const result = zapPopover.close({ restoreFocus: focusButton }); @@ -1684,7 +1685,7 @@ function handleZapButtonClick(event) { return; } - if (zapPopoverOpening) { + if (zapPopoverOpenPromise) { zapShouldFocusOnOpen = true; return; } @@ -1722,7 +1723,7 @@ async function handleZapSend(event) { } if (!isZapControlsOpen()) { - if (zapPopoverOpening) { + if (zapPopoverOpenPromise) { zapShouldFocusOnOpen = true; return; } @@ -1730,7 +1731,7 @@ async function handleZapSend(event) { return; } - if (zapPopoverOpening) { + if (zapPopoverOpenPromise) { return; } @@ -2240,6 +2241,7 @@ export async function initChannelProfileView() { currentChannelNpub = null; currentChannelLightningAddress = ""; currentChannelProfileEvent = null; + currentChannelProfileSnapshot = null; setCachedPlatformLightningAddress(""); resetZapRetryState(); clearZapReceipts(); @@ -2362,7 +2364,7 @@ function setupZapButton({ force = false } = {}) { zapPopover.destroy(); zapPopover = null; zapPopoverTrigger = null; - zapPopoverOpening = false; + zapPopoverOpenPromise = null; zapShouldFocusOnOpen = false; delete zapButton.dataset.zapPopoverWarmBound; } @@ -2482,7 +2484,7 @@ function setupZapButton({ force = false } = {}) { }); if (!popover) { - zapPopoverOpening = false; + zapPopoverOpenPromise = null; zapShouldFocusOnOpen = false; return false; } @@ -2494,7 +2496,7 @@ function setupZapButton({ force = false } = {}) { try { result = await originalOpen(...args); } catch (error) { - zapPopoverOpening = false; + zapPopoverOpenPromise = null; zapShouldFocusOnOpen = false; throw error; } @@ -2512,7 +2514,6 @@ function setupZapButton({ force = false } = {}) { } } zapShouldFocusOnOpen = false; - zapPopoverOpening = false; return result; }; } @@ -2533,7 +2534,7 @@ function setupZapButton({ force = false } = {}) { zapButton.setAttribute("aria-expanded", "false"); } zapShouldFocusOnOpen = false; - zapPopoverOpening = false; + zapPopoverOpenPromise = null; return result; }; } @@ -2546,7 +2547,7 @@ function setupZapButton({ force = false } = {}) { zapPopover = null; zapPopoverTrigger = null; } - zapPopoverOpening = false; + zapPopoverOpenPromise = null; zapShouldFocusOnOpen = false; cacheZapPanelElements(null); }; @@ -2559,6 +2560,9 @@ function setupZapButton({ force = false } = {}) { if (!zapPopover || zapPopover !== popover) { return; } + if (zapButton.hasAttribute("hidden")) { + return; + } if (typeof popover.preload !== "function") { return; } @@ -2569,8 +2573,14 @@ function setupZapButton({ force = false } = {}) { } }; - if (typeof popover.preload === "function") { - if (typeof window !== "undefined" && typeof window.requestIdleCallback === "function") { + if ( + typeof popover.preload === "function" && + !zapButton.hasAttribute("hidden") + ) { + if ( + typeof window !== "undefined" && + typeof window.requestIdleCallback === "function" + ) { window.requestIdleCallback(() => warmZapPopover(), { timeout: 250 }); } else { setTimeout(() => { @@ -3178,6 +3188,35 @@ function applyChannelProfileMetadata({ } const normalized = normalizeChannelProfileMetadata(profile); + const hasExplicitProfilePayload = + profile && + typeof profile === "object" && + Object.keys(profile).length > 0; + const incomingEventTimestamp = + typeof event?.created_at === "number" ? event.created_at : 0; + const existingEventTimestamp = + typeof currentChannelProfileEvent?.created_at === "number" + ? currentChannelProfileEvent.created_at + : 0; + + if (currentChannelProfileSnapshot) { + const isOlderEvent = + incomingEventTimestamp && + existingEventTimestamp && + incomingEventTimestamp < existingEventTimestamp; + if (isOlderEvent) { + return; + } + + const shouldIgnoreEmptyPayload = + !hasExplicitProfilePayload && + (!incomingEventTimestamp || + incomingEventTimestamp <= existingEventTimestamp); + if (shouldIgnoreEmptyPayload) { + return; + } + } + const expectedLoadToken = loadToken !== null ? loadToken : currentProfileLoadToken; @@ -3268,6 +3307,8 @@ function applyChannelProfileMetadata({ currentChannelProfileEvent = null; } + currentChannelProfileSnapshot = { ...normalized }; + if (lightningAddress) { if (lightningAddress !== previousLightning) { fetchLightningMetadata(lightningAddress) From a98705bb2f9332763354237141a298a5c5bd2ce2 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 18 Oct 2025 14:13:10 -0400 Subject: [PATCH 6/8] Preserve channel profile metadata and gate zap access --- js/channelProfile.js | 152 ++++++++++++++++++++++++++++++++++------- js/ui/zapController.js | 4 ++ 2 files changed, 131 insertions(+), 25 deletions(-) diff --git a/js/channelProfile.js b/js/channelProfile.js index edd15595..e1dbe67e 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -113,6 +113,7 @@ function logZapError(stage, details = {}, error) { } let currentChannelProfileEvent = null; let currentChannelProfileSnapshot = null; +let currentChannelProfileHasExplicitPayload = false; let cachedZapButton = null; let cachedChannelShareButton = null; @@ -146,6 +147,33 @@ const PROFILE_EVENT_CACHE_TTL_MS = 5 * 60 * 1000; const channelProfileMetadataCache = new Map(); +function hasExplicitChannelProfilePayload(profile) { + return ( + profile && + typeof profile === "object" && + Object.keys(profile).length > 0 + ); +} + +function touchChannelProfileCacheEntry(pubkey) { + if (typeof pubkey !== "string" || !pubkey) { + return false; + } + + const existing = channelProfileMetadataCache.get(pubkey); + if (!existing) { + return false; + } + + channelProfileMetadataCache.set(pubkey, { + timestamp: Date.now(), + profile: { ...existing.profile }, + event: existing.event ? { ...existing.event } : null + }); + + return true; +} + const SUPPORTED_BANNER_REFERRER_POLICIES = new Set([ "no-referrer", "origin", @@ -471,7 +499,8 @@ function setChannelZapVisibility(visible) { typeof app?.isUserLoggedIn === "function" ? app.isUserLoggedIn() : Boolean(app?.normalizeHexPubkey?.(app?.pubkey)); - const shouldShow = !!visible && isLoggedIn; + const shouldShow = !!visible; + const requiresLogin = shouldShow && !isLoggedIn; if ( shouldShow && @@ -482,7 +511,10 @@ function setChannelZapVisibility(visible) { zapButton.toggleAttribute("hidden", !shouldShow); zapButton.disabled = !shouldShow; - zapButton.setAttribute("aria-disabled", (!shouldShow).toString()); + zapButton.setAttribute( + "aria-disabled", + (requiresLogin || !shouldShow).toString() + ); zapButton.setAttribute("aria-hidden", (!shouldShow).toString()); zapButton.setAttribute("aria-expanded", "false"); if (shouldShow) { @@ -494,18 +526,24 @@ function setChannelZapVisibility(visible) { clearZapReceipts(); setZapStatus("", "neutral"); } + if (requiresLogin) { + zapButton.dataset.requiresLogin = "true"; + } else { + delete zapButton.dataset.requiresLogin; + } closeZapControls(); if (controls) { controls.setAttribute("aria-hidden", "true"); controls.hidden = true; } + const shouldEnableInputs = shouldShow && !requiresLogin; if (amountInput) { - amountInput.disabled = !shouldShow; + amountInput.disabled = !shouldEnableInputs; } if (sendButton) { - sendButton.disabled = !shouldShow; - sendButton.setAttribute("aria-hidden", (!shouldShow).toString()); - if (shouldShow) { + sendButton.disabled = !shouldEnableInputs; + sendButton.setAttribute("aria-hidden", (!shouldEnableInputs).toString()); + if (shouldEnableInputs) { sendButton.removeAttribute("tabindex"); sendButton.removeAttribute("aria-busy"); delete sendButton.dataset.state; @@ -527,12 +565,30 @@ function setChannelZapVisibility(visible) { typeof settings?.nwcUri === "string" ? settings.nwcUri.trim() : ""; return uri.length > 0; })(); - setZapWalletPromptVisible(shouldShow && !hasWallet); - if (shouldShow) { + setZapWalletPromptVisible(shouldEnableInputs && !hasWallet); + if (shouldEnableInputs) { setupZapWalletLink(); } } +function isSessionActorWithoutLogin() { + const app = getApp(); + const isLoggedIn = + typeof app?.isUserLoggedIn === "function" + ? app.isUserLoggedIn() + : Boolean(app?.normalizeHexPubkey?.(app?.pubkey) || app?.pubkey); + if (isLoggedIn) { + return false; + } + + const sessionPubkey = + typeof nostrClient?.sessionActor?.pubkey === "string" + ? nostrClient.sessionActor.pubkey.trim() + : ""; + + return sessionPubkey.length > 0; +} + function getChannelShareButton() { if ( cachedChannelShareButton && @@ -1677,6 +1733,13 @@ function handleZapButtonClick(event) { return; } + if (isSessionActorWithoutLogin()) { + const app = getApp(); + const message = "Log in with your Nostr profile to send zaps."; + app?.showError?.(message); + return; + } + if (!zapPopover || zapPopoverTrigger !== zapButton) { setupZapButton({ force: true }); } @@ -2242,6 +2305,7 @@ export async function initChannelProfileView() { currentChannelLightningAddress = ""; currentChannelProfileEvent = null; currentChannelProfileSnapshot = null; + currentChannelProfileHasExplicitPayload = false; setCachedPlatformLightningAddress(""); resetZapRetryState(); clearZapReceipts(); @@ -2707,7 +2771,30 @@ function normalizeChannelProfileMetadata(raw = {}) { function rememberChannelProfile(pubkey, { profile = {}, event = null } = {}) { if (typeof pubkey !== "string" || !pubkey) { - return; + return false; + } + + const hasPayload = hasExplicitChannelProfilePayload(profile); + if (!hasPayload) { + touchChannelProfileCacheEntry(pubkey); + return false; + } + + const incomingEventTimestamp = + typeof event?.created_at === "number" ? event.created_at : 0; + const existing = channelProfileMetadataCache.get(pubkey); + const existingEventTimestamp = + typeof existing?.event?.created_at === "number" + ? existing.event.created_at + : 0; + + if ( + existingEventTimestamp && + incomingEventTimestamp && + incomingEventTimestamp < existingEventTimestamp + ) { + touchChannelProfileCacheEntry(pubkey); + return false; } channelProfileMetadataCache.set(pubkey, { @@ -2715,6 +2802,8 @@ function rememberChannelProfile(pubkey, { profile = {}, event = null } = {}) { profile: normalizeChannelProfileMetadata(profile), event: event ? { ...event } : null }); + + return true; } function getCachedChannelProfile(pubkey) { @@ -3189,9 +3278,7 @@ function applyChannelProfileMetadata({ const normalized = normalizeChannelProfileMetadata(profile); const hasExplicitProfilePayload = - profile && - typeof profile === "object" && - Object.keys(profile).length > 0; + hasExplicitChannelProfilePayload(profile); const incomingEventTimestamp = typeof event?.created_at === "number" ? event.created_at : 0; const existingEventTimestamp = @@ -3208,12 +3295,17 @@ function applyChannelProfileMetadata({ return; } - const shouldIgnoreEmptyPayload = - !hasExplicitProfilePayload && - (!incomingEventTimestamp || - incomingEventTimestamp <= existingEventTimestamp); - if (shouldIgnoreEmptyPayload) { - return; + if (!hasExplicitProfilePayload) { + if (currentChannelProfileHasExplicitPayload) { + return; + } + + const shouldIgnoreEmptyPayload = + !incomingEventTimestamp || + incomingEventTimestamp <= existingEventTimestamp; + if (shouldIgnoreEmptyPayload) { + return; + } } } @@ -3308,6 +3400,7 @@ function applyChannelProfileMetadata({ } currentChannelProfileSnapshot = { ...normalized }; + currentChannelProfileHasExplicitPayload = hasExplicitProfilePayload; if (lightningAddress) { if (lightningAddress !== previousLightning) { @@ -3421,18 +3514,27 @@ async function loadUserProfile(pubkey) { return; } - rememberChannelProfile(pubkey, result); + const cachedUpdated = rememberChannelProfile(pubkey, result); - if (typeof app?.setProfileCacheEntry === "function") { + if (cachedUpdated && typeof app?.setProfileCacheEntry === "function") { try { - app.setProfileCacheEntry(pubkey, { - ...result.profile, - lightningAddress: + const profileForCache = + result && typeof result.profile === "object" && result.profile + ? { ...result.profile } + : {}; + + if (!profileForCache.lightningAddress) { + const lightningFallback = result.profile?.lightningAddress || result.profile?.lud16 || result.profile?.lud06 || - "" - }); + ""; + if (lightningFallback) { + profileForCache.lightningAddress = lightningFallback; + } + } + + app.setProfileCacheEntry(pubkey, profileForCache); } catch (error) { userLogger.warn( "Failed to persist channel profile metadata to cache:", diff --git a/js/ui/zapController.js b/js/ui/zapController.js index e4135f7b..fca1cd71 100644 --- a/js/ui/zapController.js +++ b/js/ui/zapController.js @@ -117,6 +117,10 @@ export default class ZapController { open() { this.resetFeedback(); + if (!this.isUserLoggedIn()) { + this.notifyError("Log in with your Nostr profile to send zaps."); + return; + } this.applyDefaultAmount(); this.preloadLightningMetadata().catch((error) => { devLogger.warn("[zap] Preload metadata error:", error); From d7b0faffa247f1a6a8ed00b8268d4baffd3caf5d Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 18 Oct 2025 14:33:13 -0400 Subject: [PATCH 7/8] Improve zap login prompts and wallet pane handling --- js/app.js | 34 +++++++++++++++++-- js/channelProfile.js | 31 ++++++++++++++---- js/ui/components/VideoModal.js | 58 ++++++++++++++++++++++++++------- js/ui/profileModalController.js | 9 +++++ js/ui/zapController.js | 18 ++++++++-- 5 files changed, 127 insertions(+), 23 deletions(-) diff --git a/js/app.js b/js/app.js index f1959c2d..6f95ad60 100644 --- a/js/app.js +++ b/js/app.js @@ -586,14 +586,18 @@ class Application { getCurrentVideo: () => this.currentVideo, nwcSettings: this.nwcSettingsService, isUserLoggedIn: () => this.isUserLoggedIn(), + hasSessionActor: () => + Boolean( + typeof nostrClient?.sessionActor?.pubkey === "string" && + nostrClient.sessionActor.pubkey.trim() + ), splitAndZap: (...args) => this.splitAndZap(...args), payments: this.payments, callbacks: { onSuccess: (message) => this.showSuccess(message), onError: (message) => this.showError(message), }, - requestWalletPane: () => - this.profileController?.showWalletPane?.(), + requestWalletPane: () => this.openWalletPane(), }); this.boundVideoModalCloseHandler = () => { this.hideModal(); @@ -1308,6 +1312,32 @@ class Application { } } + openWalletPane() { + if ( + !this.profileController || + typeof this.profileController.showWalletPane !== "function" + ) { + devLogger.warn( + "[Application] Wallet pane requested before profile controller initialized.", + ); + return Promise.resolve(false); + } + + try { + const result = this.profileController.showWalletPane(); + if (result && typeof result.then === "function") { + return result.catch((error) => { + devLogger.error("[Application] Failed to open wallet pane:", error); + throw error; + }); + } + return Promise.resolve(result); + } catch (error) { + devLogger.error("[Application] Failed to open wallet pane:", error); + return Promise.reject(error); + } + } + normalizeModalTrigger(candidate) { if (!candidate) { return null; diff --git a/js/channelProfile.js b/js/channelProfile.js index e1dbe67e..92a1461e 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -499,7 +499,10 @@ function setChannelZapVisibility(visible) { typeof app?.isUserLoggedIn === "function" ? app.isUserLoggedIn() : Boolean(app?.normalizeHexPubkey?.(app?.pubkey)); - const shouldShow = !!visible; + const sessionActorPubkey = getSessionActorPubkey(); + const hasSessionActor = sessionActorPubkey.length > 0; + const sessionOnly = !isLoggedIn && hasSessionActor; + const shouldShow = !!visible && (isLoggedIn || sessionOnly); const requiresLogin = shouldShow && !isLoggedIn; if ( @@ -581,12 +584,13 @@ function isSessionActorWithoutLogin() { return false; } - const sessionPubkey = - typeof nostrClient?.sessionActor?.pubkey === "string" - ? nostrClient.sessionActor.pubkey.trim() - : ""; + return getSessionActorPubkey().length > 0; +} - return sessionPubkey.length > 0; +function getSessionActorPubkey() { + return typeof nostrClient?.sessionActor?.pubkey === "string" + ? nostrClient.sessionActor.pubkey.trim() + : ""; } function getChannelShareButton() { @@ -970,7 +974,20 @@ function setupZapWalletLink() { event?.preventDefault?.(); const app = getApp(); if (typeof app?.openWalletPane === "function") { - app.openWalletPane(); + Promise.resolve() + .then(() => app.openWalletPane()) + .catch((error) => { + devLogger.warn( + "[zap] Failed to open wallet pane from channel view:", + error + ); + app?.showError?.("Wallet settings are not available right now."); + }); + } else { + devLogger.warn( + "[zap] Wallet pane requested but application did not expose openWalletPane()." + ); + app?.showError?.("Wallet settings are not available right now."); } }); link.dataset.initialized = "true"; diff --git a/js/ui/components/VideoModal.js b/js/ui/components/VideoModal.js index 7f1264cb..b536062a 100644 --- a/js/ui/components/VideoModal.js +++ b/js/ui/components/VideoModal.js @@ -77,6 +77,7 @@ export class VideoModal { this.modalZapWalletLink = null; this.modalZapDialogOpen = false; this.modalZapPending = false; + this.modalZapRequiresLogin = false; this.modalZapPopover = null; this.modalMorePopover = null; @@ -414,6 +415,14 @@ export class VideoModal { return; } + if (this.modalZapRequiresLogin) { + this.dispatch("zap:open", { + video: this.activeVideo, + requiresLogin: true, + }); + return; + } + const popoverIsOpen = typeof this.modalZapPopover?.isOpen === "function" ? this.modalZapPopover.isOpen() @@ -966,13 +975,29 @@ export class VideoModal { this.shareBtn.classList.toggle("cursor-not-allowed", !enabled); } - setZapVisibility(visible) { - const shouldShow = !!visible; + setZapVisibility(visible, options = {}) { + let config; + if (typeof visible === "object" && visible !== null) { + config = { ...visible }; + } else { + config = { + visible, + ...(options && typeof options === "object" ? options : {}), + }; + } + + const shouldShow = !!config.visible; + const requiresLogin = shouldShow && !!config.requiresLogin; + this.modalZapRequiresLogin = requiresLogin; + if (this.modalZapBtn) { this.modalZapBtn.toggleAttribute("hidden", !shouldShow); - const disableButton = !shouldShow || this.modalZapPending; + const disableButton = + !shouldShow || (this.modalZapPending && !requiresLogin); this.modalZapBtn.disabled = disableButton; - this.modalZapBtn.setAttribute("aria-disabled", (!shouldShow).toString()); + const ariaDisabledValue = + requiresLogin || !shouldShow ? "true" : "false"; + this.modalZapBtn.setAttribute("aria-disabled", ariaDisabledValue); this.modalZapBtn.setAttribute("aria-hidden", (!shouldShow).toString()); this.modalZapBtn.setAttribute("aria-expanded", "false"); if (shouldShow) { @@ -980,16 +1005,26 @@ export class VideoModal { } else { this.modalZapBtn.setAttribute("tabindex", "-1"); } - if (this.modalZapPending) { - this.modalZapBtn.setAttribute("aria-busy", "true"); - this.modalZapBtn.classList.add("opacity-50", "pointer-events-none"); - } else { + if (requiresLogin) { + this.modalZapBtn.dataset.requiresLogin = "true"; this.modalZapBtn.removeAttribute("aria-busy"); this.modalZapBtn.classList.remove("opacity-50", "pointer-events-none"); + } else { + delete this.modalZapBtn.dataset.requiresLogin; + if (this.modalZapPending) { + this.modalZapBtn.setAttribute("aria-busy", "true"); + this.modalZapBtn.classList.add("opacity-50", "pointer-events-none"); + } else { + this.modalZapBtn.removeAttribute("aria-busy"); + this.modalZapBtn.classList.remove( + "opacity-50", + "pointer-events-none", + ); + } } } - if (!shouldShow) { + if (!shouldShow || requiresLogin) { this.closeZapDialog({ silent: true, restoreFocus: false }); } } @@ -1292,11 +1327,12 @@ export class VideoModal { } if (this.modalZapBtn) { - if (isPending) { + const buttonHidden = this.modalZapBtn.hasAttribute("hidden"); + if (isPending && !this.modalZapRequiresLogin) { this.modalZapBtn.disabled = true; this.modalZapBtn.setAttribute("aria-busy", "true"); this.modalZapBtn.classList.add("opacity-50", "pointer-events-none"); - } else if (!this.modalZapBtn.hasAttribute("hidden")) { + } else if (!buttonHidden) { this.modalZapBtn.disabled = false; this.modalZapBtn.removeAttribute("aria-busy"); this.modalZapBtn.classList.remove("opacity-50", "pointer-events-none"); diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js index b5dfc0c5..fc119e35 100644 --- a/js/ui/profileModalController.js +++ b/js/ui/profileModalController.js @@ -4016,6 +4016,15 @@ export class ProfileModalController { return; } + const parentElement = modalRoot.parentElement; + if (parentElement) { + if (parentElement.lastElementChild !== modalRoot) { + parentElement.appendChild(modalRoot); + } + } else if (typeof document !== "undefined" && document.body) { + document.body.appendChild(modalRoot); + } + modalRoot.classList.remove("hidden"); modalRoot.setAttribute("aria-hidden", "false"); this.setGlobalModalState("profile", true); diff --git a/js/ui/zapController.js b/js/ui/zapController.js index fca1cd71..f7b9541b 100644 --- a/js/ui/zapController.js +++ b/js/ui/zapController.js @@ -33,6 +33,7 @@ export default class ZapController { nwcSettings, getActiveNwcSettings, isUserLoggedIn, + hasSessionActor, hasActiveWalletConnection, splitAndZap, payments = {}, @@ -58,6 +59,8 @@ export default class ZapController { } this.isUserLoggedIn = typeof isUserLoggedIn === "function" ? isUserLoggedIn : () => false; + this.hasSessionActor = + typeof hasSessionActor === "function" ? hasSessionActor : () => false; if ( this.nwcSettings && typeof this.nwcSettings.hasActiveWalletConnection === "function" @@ -89,11 +92,20 @@ export default class ZapController { setVisibility(visible) { const lightningVisible = !!visible; - const shouldShow = lightningVisible && this.isUserLoggedIn(); + const loggedIn = this.isUserLoggedIn(); + const sessionActorAvailable = this.hasSessionActor(); + const shouldShow = + lightningVisible && (loggedIn || sessionActorAvailable); + const requiresLogin = shouldShow && !loggedIn; const hasWallet = this.hasActiveWalletConnection(); if (this.videoModal) { - this.videoModal.setZapVisibility(shouldShow); - this.videoModal.setWalletPromptVisible(shouldShow && !hasWallet); + this.videoModal.setZapVisibility({ + visible: shouldShow, + requiresLogin, + }); + this.videoModal.setWalletPromptVisible( + shouldShow && !requiresLogin && !hasWallet, + ); } } From 19957d398f0006326b417eaccfabf728841d9c81 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 18 Oct 2025 14:51:51 -0400 Subject: [PATCH 8/8] Improve zap popover toggling and logout handling --- js/app.js | 77 ++++++++++++++++++++++++++++++++++ js/channelProfile.js | 45 ++++++++++++++++++++ js/services/nostrService.js | 7 ++++ js/ui/components/VideoModal.js | 66 +++++++++++++++++++++-------- 4 files changed, 178 insertions(+), 17 deletions(-) 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) {