diff --git a/css/style.css b/css/style.css
index 456ca6c6..dea54de6 100644
--- a/css/style.css
+++ b/css/style.css
@@ -1016,6 +1016,19 @@ 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);
+}
+
.modal-actions__overflow {
position: relative;
overflow: visible;
diff --git a/js/app.js b/js/app.js
index 95ebb19c..fd850e14 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);
}
}
@@ -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..7d634d17 100644
--- a/js/channelProfile.js
+++ b/js/channelProfile.js
@@ -109,9 +109,13 @@ let cachedZapAmountInput = null;
let cachedZapSplitSummary = null;
let cachedZapStatus = null;
let cachedZapReceipts = null;
+let cachedZapWalletPrompt = null;
+let cachedZapWalletLink = null;
+let cachedZapCloseBtn = null;
let pendingZapRetry = null;
let zapInFlight = false;
+let zapControlsOpen = false;
function getChannelZapButton() {
if (cachedZapButton && !document.body.contains(cachedZapButton)) {
@@ -126,28 +130,53 @@ function getChannelZapButton() {
function setChannelZapVisibility(visible) {
const zapButton = getChannelZapButton();
const controls = getZapControlsContainer();
+ const amountInput = getZapAmountInput();
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;
+ }
+
+ 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();
}
}
@@ -221,6 +250,114 @@ function getZapReceiptsList() {
return cachedZapReceipts;
}
+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 +525,8 @@ 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";
}
}
@@ -823,6 +960,11 @@ async function handleZapButtonClick(event) {
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.");
@@ -1160,6 +1302,18 @@ function setupZapButton() {
}
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();
diff --git a/js/ui/components/VideoModal.js b/js/ui/components/VideoModal.js
index cb05fd1b..701db469 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 });
}
diff --git a/views/channel-profile.html b/views/channel-profile.html
index c0940877..9ba77323 100644
--- a/views/channel-profile.html
+++ b/views/channel-profile.html
@@ -29,18 +29,96 @@
-
+
+
+
+
+
+
+ Send a zap
+
+
+
+
+ Connect a wallet in
+
+ to send zaps.
+
+
+
+
+ Enter an amount to view the split.
+
+
+
+
+
+
-
-
-
-
-
- Enter an amount to view the split.
-
-
-
-
-