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 @@
-
+
+
+
+
+
+
+ Send a zap
+
+
+
+
+ Connect a wallet in
+
+ to send zaps.
+
+
+
+
+
-
-
-
-
-
- Enter an amount to view the split.
-
-
-
-
-