diff --git a/components/video-modal.html b/components/video-modal.html index 39339e5f..3045c03f 100644 --- a/components/video-modal.html +++ b/components/video-modal.html @@ -35,18 +35,124 @@ class="icon-image" /> - +
+ + +
- -
-
- - -
-
- - -
-

- Enter an amount to view the split. -

-

- -
- -
-
- - -
diff --git a/css/style.css b/css/style.css index 456ca6c6..aad9283e 100644 --- a/css/style.css +++ b/css/style.css @@ -1016,6 +1016,25 @@ body.modal-open #app { flex-wrap: nowrap; } +.zap-popover { + position: absolute; + top: calc(100% + 0.5rem); + right: 0; + z-index: 80; + width: max-content; +} + +.zap-popover__content { + width: 18rem; + max-width: calc(100vw - 2rem); + background: rgba(11, 17, 32, 0.96); + color: #e5e7eb; + border: 1px solid rgba(148, 163, 184, 0.35); + box-shadow: 0 22px 48px -18px rgba(8, 14, 28, 0.7); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(12px); +} + .modal-actions__overflow { position: relative; overflow: visible; diff --git a/js/app.js b/js/app.js index 95ebb19c..008aadf6 100644 --- a/js/app.js +++ b/js/app.js @@ -198,6 +198,7 @@ class Application { this.boundVideoModalShareHandler = null; this.boundVideoModalCreatorHandler = null; this.boundVideoModalZapHandler = null; + this.boundVideoModalZapWalletHandler = null; this.videoListViewPlaybackHandler = null; this.videoListViewEditHandler = null; this.videoListViewRevertHandler = null; @@ -529,6 +530,15 @@ class Application { "zap:comment-change", this.boundVideoModalZapCommentHandler ); + this.boundVideoModalZapWalletHandler = () => { + if (typeof this.openWalletPane === "function") { + this.openWalletPane(); + } + }; + this.videoModal.addEventListener( + "zap:wallet-link", + this.boundVideoModalZapWalletHandler + ); // Hide/Show Subscriptions Link this.subscriptionsLink = null; @@ -4512,6 +4522,17 @@ class Application { return cached ? { ...cached } : createDefaultNwcSettings(); } + hasActiveWalletConnection() { + const settings = this.getActiveNwcSettings(); + const candidate = + typeof settings?.nwcUri === "string" ? settings.nwcUri.trim() : ""; + return candidate.length > 0; + } + + isUserLoggedIn() { + return Boolean(this.normalizeHexPubkey(this.pubkey)); + } + async updateActiveNwcSettings(partial = {}) { const normalized = this.normalizeHexPubkey(this.pubkey); if (!normalized) { @@ -5726,8 +5747,12 @@ class Application { } setModalZapVisibility(visible) { + const lightningVisible = !!visible; + const shouldShow = lightningVisible && this.isUserLoggedIn(); + const hasWallet = this.hasActiveWalletConnection(); if (this.videoModal) { - this.videoModal.setZapVisibility(visible); + this.videoModal.setZapVisibility(shouldShow); + this.videoModal.setWalletPromptVisible(shouldShow && !hasWallet); } } @@ -6578,8 +6603,8 @@ class Application { const tone = tracker.length > failureShares.length ? "warning" : "error"; const statusMessage = tracker.length > failureShares.length - ? `Partial zap failure. Tap zap again to retry: ${summary}.` - : `Zap failed. Tap zap again to retry: ${summary}.`; + ? `Partial zap failure. Press Send again to retry: ${summary}.` + : `Zap failed. Press Send again to retry: ${summary}.`; this.videoModal?.setZapStatus(statusMessage, tone); this.showError(error?.message || statusMessage); } else { @@ -9479,6 +9504,13 @@ class Application { ); this.boundVideoModalZapCommentHandler = null; } + if (this.boundVideoModalZapWalletHandler) { + this.videoModal.removeEventListener( + "zap:wallet-link", + this.boundVideoModalZapWalletHandler + ); + this.boundVideoModalZapWalletHandler = null; + } if (typeof this.videoModal.destroy === "function") { try { this.videoModal.destroy(); diff --git a/js/channelProfile.js b/js/channelProfile.js index 88b39528..20cb7549 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -105,13 +105,19 @@ let cachedZapButton = null; let cachedChannelShareButton = null; let cachedChannelMenu = null; let cachedZapControls = null; +let cachedZapForm = null; let cachedZapAmountInput = null; let cachedZapSplitSummary = null; let cachedZapStatus = null; let cachedZapReceipts = null; +let cachedZapWalletPrompt = null; +let cachedZapWalletLink = null; +let cachedZapCloseBtn = null; +let cachedZapSendBtn = null; let pendingZapRetry = null; let zapInFlight = false; +let zapControlsOpen = false; function getChannelZapButton() { if (cachedZapButton && !document.body.contains(cachedZapButton)) { @@ -126,28 +132,65 @@ function getChannelZapButton() { function setChannelZapVisibility(visible) { const zapButton = getChannelZapButton(); const controls = getZapControlsContainer(); + const amountInput = getZapAmountInput(); + const sendButton = getZapSendButton(); if (!zapButton) { return; } - const shouldShow = !!visible; + const app = getApp(); + const isLoggedIn = + typeof app?.isUserLoggedIn === "function" + ? app.isUserLoggedIn() + : Boolean(app?.normalizeHexPubkey?.(app?.pubkey)); + const shouldShow = !!visible && isLoggedIn; zapButton.classList.toggle("hidden", !shouldShow); zapButton.disabled = !shouldShow; zapButton.setAttribute("aria-disabled", (!shouldShow).toString()); zapButton.setAttribute("aria-hidden", (!shouldShow).toString()); + zapButton.setAttribute("aria-expanded", "false"); if (shouldShow) { zapButton.removeAttribute("tabindex"); } else { zapButton.setAttribute("tabindex", "-1"); - if (controls) { - controls.classList.add("hidden"); - } resetZapRetryState(); zapInFlight = false; clearZapReceipts(); setZapStatus("", "neutral"); } + closeZapControls(); if (controls) { - controls.classList.toggle("hidden", !shouldShow); + controls.setAttribute("aria-hidden", "true"); + } + if (amountInput) { + amountInput.disabled = !shouldShow; + } + if (sendButton) { + sendButton.disabled = !shouldShow; + sendButton.setAttribute("aria-hidden", (!shouldShow).toString()); + if (shouldShow) { + sendButton.removeAttribute("tabindex"); + sendButton.removeAttribute("aria-busy"); + sendButton.classList.remove("opacity-50", "pointer-events-none"); + } else { + sendButton.setAttribute("tabindex", "-1"); + } + } + + const hasWallet = + typeof app?.hasActiveWalletConnection === "function" + ? app.hasActiveWalletConnection() + : (() => { + const settings = + typeof app?.getActiveNwcSettings === "function" + ? app.getActiveNwcSettings() + : {}; + const uri = + typeof settings?.nwcUri === "string" ? settings.nwcUri.trim() : ""; + return uri.length > 0; + })(); + setZapWalletPromptVisible(shouldShow && !hasWallet); + if (shouldShow) { + setupZapWalletLink(); } } @@ -181,6 +224,16 @@ function getZapControlsContainer() { return cachedZapControls; } +function getZapFormElement() { + if (cachedZapForm && !document.body.contains(cachedZapForm)) { + cachedZapForm = null; + } + if (!cachedZapForm) { + cachedZapForm = document.getElementById("zapForm"); + } + return cachedZapForm; +} + function getZapAmountInput() { if (cachedZapAmountInput && !document.body.contains(cachedZapAmountInput)) { cachedZapAmountInput = null; @@ -221,6 +274,124 @@ function getZapReceiptsList() { return cachedZapReceipts; } +function getZapSendButton() { + if (cachedZapSendBtn && !document.body.contains(cachedZapSendBtn)) { + cachedZapSendBtn = null; + } + if (!cachedZapSendBtn) { + cachedZapSendBtn = document.getElementById("zapSendBtn"); + } + return cachedZapSendBtn; +} + +function getZapWalletPrompt() { + if (cachedZapWalletPrompt && !document.body.contains(cachedZapWalletPrompt)) { + cachedZapWalletPrompt = null; + } + if (!cachedZapWalletPrompt) { + cachedZapWalletPrompt = document.getElementById("zapWalletPrompt"); + } + return cachedZapWalletPrompt; +} + +function getZapWalletLink() { + if (cachedZapWalletLink && !document.body.contains(cachedZapWalletLink)) { + cachedZapWalletLink = null; + } + if (!cachedZapWalletLink) { + cachedZapWalletLink = document.getElementById("zapWalletLink"); + } + return cachedZapWalletLink; +} + +function getZapCloseButton() { + if (cachedZapCloseBtn && !document.body.contains(cachedZapCloseBtn)) { + cachedZapCloseBtn = null; + } + if (!cachedZapCloseBtn) { + cachedZapCloseBtn = document.getElementById("zapCloseBtn"); + } + return cachedZapCloseBtn; +} + +function setZapWalletPromptVisible(visible) { + const prompt = getZapWalletPrompt(); + if (!prompt) { + return; + } + const shouldShow = !!visible; + prompt.classList.toggle("hidden", !shouldShow); + prompt.setAttribute("aria-hidden", (!shouldShow).toString()); +} + +function setupZapWalletLink() { + const link = getZapWalletLink(); + if (!link) { + return; + } + if (link.dataset.initialized === "true") { + return; + } + link.addEventListener("click", (event) => { + event?.preventDefault?.(); + const app = getApp(); + if (typeof app?.openWalletPane === "function") { + app.openWalletPane(); + } + }); + link.dataset.initialized = "true"; +} + +function focusZapAmountField() { + const amountInput = getZapAmountInput(); + if (amountInput && typeof amountInput.focus === "function") { + try { + amountInput.focus({ preventScroll: true }); + } catch (error) { + amountInput.focus(); + } + } +} + +function isZapControlsOpen() { + return zapControlsOpen; +} + +function openZapControls({ focus = false } = {}) { + const controls = getZapControlsContainer(); + const zapButton = getChannelZapButton(); + if (!controls || !zapButton) { + return false; + } + if (!zapControlsOpen) { + controls.classList.remove("hidden"); + controls.setAttribute("aria-hidden", "false"); + zapButton.setAttribute("aria-expanded", "true"); + zapControlsOpen = true; + } + if (focus) { + focusZapAmountField(); + } + return true; +} + +function closeZapControls({ focusButton = false } = {}) { + const controls = getZapControlsContainer(); + const zapButton = getChannelZapButton(); + if (controls) { + controls.classList.add("hidden"); + controls.setAttribute("aria-hidden", "true"); + } + if (zapButton) { + zapButton.setAttribute("aria-expanded", "false"); + if (focusButton && typeof zapButton.focus === "function") { + zapButton.focus(); + } + } + zapControlsOpen = false; + return Boolean(controls && zapButton); +} + function updateZapSplitSummary({ overrideFee = null } = {}) { const summaryEl = getZapSplitSummaryElement(); if (!summaryEl) { @@ -388,8 +559,15 @@ function resetZapRetryState() { const zapButton = getChannelZapButton(); if (zapButton) { delete zapButton.dataset.retryPending; - zapButton.setAttribute("aria-label", "Send a zap"); - zapButton.title = "Send a zap"; + zapButton.setAttribute("aria-label", "Open zap dialog"); + zapButton.title = "Open zap dialog"; + } + const sendButton = getZapSendButton(); + if (sendButton) { + delete sendButton.dataset.retryPending; + sendButton.textContent = "Send"; + sendButton.setAttribute("aria-label", "Send a zap"); + sendButton.removeAttribute("title"); } } @@ -409,6 +587,13 @@ function markZapRetryPending(shares) { zapButton.setAttribute("aria-label", "Retry failed zap shares"); zapButton.title = "Retry failed zap shares"; } + const sendButton = getZapSendButton(); + if (sendButton) { + sendButton.dataset.retryPending = "true"; + sendButton.textContent = "Retry"; + sendButton.setAttribute("aria-label", "Retry failed zap shares"); + sendButton.title = "Retry failed zap shares"; + } } function getZapVideoEvent() { @@ -810,22 +995,51 @@ async function executePendingRetry({ walletSettings }) { resetZapRetryState(); } -async function handleZapButtonClick(event) { +function handleZapButtonClick(event) { if (event) { event.preventDefault(); } + const zapButton = getChannelZapButton(); + if (!zapButton) { + return; + } + + if (!isZapControlsOpen()) { + openZapControls({ focus: true }); + return; + } + + if (zapInFlight) { + return; + } + + closeZapControls({ focusButton: true }); +} + +async function handleZapSend(event) { + if (event?.preventDefault) { + event.preventDefault(); + } + const zapButton = getChannelZapButton(); const amountInput = getZapAmountInput(); + const sendButton = getZapSendButton(); const app = getApp(); - if (!zapButton || !amountInput) { + if (!amountInput || !sendButton) { + return; + } + + if (!isZapControlsOpen()) { + openZapControls({ focus: true }); return; } if (!currentChannelLightningAddress) { - setZapStatus("This creator has not configured a Lightning address yet.", "error"); - app?.showError?.("This creator has not configured a Lightning address yet."); + const message = "This creator has not configured a Lightning address yet."; + setZapStatus(message, "error"); + app?.showError?.(message); return; } @@ -839,9 +1053,13 @@ async function handleZapButtonClick(event) { } zapInFlight = true; - zapButton.disabled = true; - zapButton.setAttribute("aria-busy", "true"); - zapButton.classList.add("opacity-50", "pointer-events-none"); + if (zapButton) { + zapButton.disabled = true; + zapButton.setAttribute("aria-disabled", "true"); + } + sendButton.disabled = true; + sendButton.setAttribute("aria-busy", "true"); + sendButton.classList.add("opacity-50", "pointer-events-none"); amountInput.disabled = true; let attemptedAmount = null; @@ -886,7 +1104,7 @@ async function handleZapButtonClick(event) { ? error.__zapShareTracker : []; logZapError( - "handleZapButtonClick", + "handleZapSend", { amount: attemptedAmount, walletSettings, @@ -913,8 +1131,8 @@ async function handleZapButtonClick(event) { const tone = tracker.length > failureShares.length ? "warning" : "error"; const statusMessage = tracker.length > failureShares.length - ? `Partial zap failure. Tap zap again to retry: ${summary}.` - : `Zap failed. Tap zap again to retry: ${summary}.`; + ? `Partial zap failure. Press Send again to retry: ${summary}.` + : `Zap failed. Press Send again to retry: ${summary}.`; setZapStatus(statusMessage, tone); app?.showError?.(error?.message || statusMessage); } else { @@ -925,9 +1143,13 @@ async function handleZapButtonClick(event) { } } finally { zapInFlight = false; - zapButton.disabled = false; - zapButton.removeAttribute("aria-busy"); - zapButton.classList.remove("opacity-50", "pointer-events-none"); + if (zapButton) { + zapButton.disabled = false; + zapButton.removeAttribute("aria-disabled"); + } + sendButton.disabled = false; + sendButton.removeAttribute("aria-busy"); + sendButton.classList.remove("opacity-50", "pointer-events-none"); amountInput.disabled = false; } } @@ -1155,11 +1377,24 @@ function setupZapButton() { const zapButton = getChannelZapButton(); const amountInput = getZapAmountInput(); const controls = getZapControlsContainer(); - if (!zapButton || !amountInput || !controls) { + const zapForm = getZapFormElement(); + if (!zapButton || !amountInput || !controls || !zapForm || !getZapSendButton()) { return; } setChannelZapVisibility(false); + controls.classList.add("hidden"); + controls.setAttribute("aria-hidden", "true"); + zapButton.setAttribute("aria-expanded", "false"); + setupZapWalletLink(); + const closeBtn = getZapCloseButton(); + if (closeBtn && closeBtn.dataset.initialized !== "true") { + closeBtn.addEventListener("click", (event) => { + event?.preventDefault?.(); + closeZapControls({ focusButton: true }); + }); + closeBtn.dataset.initialized = "true"; + } if (zapButton.dataset.initialized === "true") { updateZapSplitSummary(); @@ -1178,6 +1413,10 @@ function setupZapButton() { updateZapSplitSummary(); amountInput.addEventListener("input", handleZapAmountChange); amountInput.addEventListener("change", handleZapAmountChange); + if (zapForm.dataset.initialized !== "true") { + zapForm.addEventListener("submit", handleZapSend); + zapForm.dataset.initialized = "true"; + } zapButton.addEventListener("click", handleZapButtonClick); zapButton.dataset.initialized = "true"; } diff --git a/js/ui/components/VideoModal.js b/js/ui/components/VideoModal.js index cb05fd1b..4993d2fd 100644 --- a/js/ui/components/VideoModal.js +++ b/js/ui/components/VideoModal.js @@ -54,6 +54,8 @@ export class VideoModal { this.modalZapReceipts = null; this.modalZapSendBtn = null; this.modalZapCloseBtn = null; + this.modalZapWalletPrompt = null; + this.modalZapWalletLink = null; this.modalZapDialogOpen = false; this.modalZapPending = false; @@ -208,6 +210,10 @@ export class VideoModal { playerModal.querySelector("#modalZapSendBtn") || null; this.modalZapCloseBtn = playerModal.querySelector("#modalZapCloseBtn") || null; + this.modalZapWalletPrompt = + playerModal.querySelector("#modalZapWalletPrompt") || null; + this.modalZapWalletLink = + playerModal.querySelector("#modalZapWalletLink") || null; const closeButton = playerModal.querySelector("#closeModal"); if (closeButton) { @@ -307,6 +313,13 @@ export class VideoModal { }); } + if (this.modalZapWalletLink) { + this.modalZapWalletLink.addEventListener("click", (event) => { + event?.preventDefault?.(); + this.dispatch("zap:wallet-link", { video: this.activeVideo }); + }); + } + if (this.modalZapForm) { this.modalZapForm.addEventListener("submit", (event) => { event?.preventDefault?.(); @@ -539,6 +552,7 @@ export class VideoModal { this.modalZapBtn.disabled = disableButton; this.modalZapBtn.setAttribute("aria-disabled", (!shouldShow).toString()); this.modalZapBtn.setAttribute("aria-hidden", (!shouldShow).toString()); + this.modalZapBtn.setAttribute("aria-expanded", "false"); if (shouldShow) { this.modalZapBtn.removeAttribute("tabindex"); } else { @@ -558,6 +572,18 @@ export class VideoModal { } } + setWalletPromptVisible(visible) { + if (!this.modalZapWalletPrompt) { + return; + } + const shouldShow = !!visible; + this.modalZapWalletPrompt.classList.toggle("hidden", !shouldShow); + this.modalZapWalletPrompt.setAttribute( + "aria-hidden", + (!shouldShow).toString() + ); + } + openZapDialog() { if (!this.modalZapDialog) { return; @@ -565,6 +591,9 @@ export class VideoModal { this.modalZapDialog.classList.remove("hidden"); this.modalZapDialogOpen = true; this.modalZapDialog.setAttribute("aria-hidden", "false"); + if (this.modalZapBtn) { + this.modalZapBtn.setAttribute("aria-expanded", "true"); + } this.focusZapAmount(); } @@ -576,6 +605,9 @@ export class VideoModal { this.modalZapDialog.classList.add("hidden"); this.modalZapDialog.setAttribute("aria-hidden", "true"); this.modalZapDialogOpen = false; + if (this.modalZapBtn) { + this.modalZapBtn.setAttribute("aria-expanded", "false"); + } if (!silent) { this.dispatch("zap:close", { video: this.activeVideo }); } @@ -825,7 +857,7 @@ export class VideoModal { if (pending) { this.modalZapSendBtn.dataset.retryPending = "true"; - this.modalZapSendBtn.textContent = "Retry zap"; + this.modalZapSendBtn.textContent = "Retry"; this.modalZapSendBtn.setAttribute( "aria-label", "Retry failed zap shares" @@ -835,7 +867,7 @@ export class VideoModal { } } else { delete this.modalZapSendBtn.dataset.retryPending; - this.modalZapSendBtn.textContent = "Send zap"; + this.modalZapSendBtn.textContent = "Send"; this.modalZapSendBtn.setAttribute("aria-label", "Send a zap"); this.modalZapSendBtn.removeAttribute("title"); } diff --git a/views/channel-profile.html b/views/channel-profile.html index c0940877..90747dd6 100644 --- a/views/channel-profile.html +++ b/views/channel-profile.html @@ -29,18 +29,110 @@
- +
+ + +
-