Fix zap dialog positioning and gating

This commit is contained in:
thePR0M3TH3AN committed 2025-10-05 18:24:32 -04:00
1 parent 5d04571b6b
commit 0fd54abde8
6 files changed
+447 -145

No files matched your search

+118 -95
View File
@@ -35,18 +35,124 @@
class="icon-image"
/>
</button>
<button
id="modalZapBtn"
type="button"
class="icon-button"
aria-label="Send a zap"
>
<img
src="assets/svg/lightning-bolt.svg"
alt="Zap"
class="icon-image"
/>
</button>
<div class="relative" data-modal-zap-wrapper>
<button
id="modalZapBtn"
type="button"
class="icon-button"
aria-label="Send a zap"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="modalZapDialog"
>
<img
src="assets/svg/lightning-bolt.svg"
alt="Zap"
class="icon-image"
/>
</button>
<div
id="modalZapDialog"
class="zap-popover hidden"
role="dialog"
aria-hidden="true"
>
<div
class="zap-popover__content w-72 max-w-[calc(100vw-2rem)] rounded-lg bg-gray-900/95 p-4 shadow-lg backdrop-blur"
>
<div class="mb-3 flex items-center justify-between gap-4">
<h3
class="text-xs font-semibold uppercase tracking-wide text-gray-300"
>
Send a zap
</h3>
<button
type="button"
id="modalZapCloseBtn"
class="text-gray-400 transition-colors hover:text-gray-200"
aria-label="Close zap dialog"
>
✕
</button>
</div>
<p
id="modalZapWalletPrompt"
class="mb-3 hidden text-sm text-gray-200"
aria-hidden="true"
>
Connect a wallet in
<button
type="button"
id="modalZapWalletLink"
class="font-semibold text-blue-300 underline decoration-dotted transition-colors hover:text-blue-200"
>
Wallet Connect settings
</button>
to send zaps.
</p>
<form id="modalZapForm" class="space-y-3">
<div>
<label
for="modalZapAmountInput"
class="mb-2 block text-xs font-semibold uppercase tracking-wide text-gray-300"
>
Zap amount (sats)
</label>
<input
id="modalZapAmountInput"
type="number"
min="1"
step="1"
inputmode="numeric"
class="w-full rounded border border-gray-600 bg-gray-800 px-3 py-2 text-sm text-gray-100 focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400"
placeholder="Enter sats"
/>
</div>
<div>
<label
for="modalZapCommentInput"
class="mb-2 block text-xs font-semibold uppercase tracking-wide text-gray-300"
>
Comment (optional)
</label>
<textarea
id="modalZapCommentInput"
rows="2"
maxlength="280"
class="w-full resize-y rounded border border-gray-600 bg-gray-800 px-3 py-2 text-sm text-gray-100 focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400"
placeholder="Include a note with your zap"
></textarea>
</div>
<p
id="modalZapSplitSummary"
class="text-sm text-gray-300"
aria-live="polite"
>
Enter an amount to view the split.
</p>
<p
id="modalZapStatus"
class="text-xs text-gray-400"
role="status"
aria-live="polite"
></p>
<ul
id="modalZapReceipts"
class="space-y-2 text-xs text-gray-300"
></ul>
<div class="flex items-center justify-end gap-2">
<button
type="submit"
id="modalZapSendBtn"
class="rounded bg-blue-500 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-gray-900"
>
Send zap
</button>
</div>
</form>
</div>
</div>
</div>
<button id="shareBtn" class="icon-button">
<img
src="assets/svg/share-video.svg"
@@ -167,89 +273,6 @@
</p>
</div>
<!-- Zap dialog -->
<div id="modalZapDialog" class="hidden mt-4" aria-hidden="true">
<div
class="w-full rounded-lg bg-gray-900 bg-opacity-70 p-4 shadow-lg backdrop-blur"
>
<div class="flex items-center justify-between mb-3">
<h3
class="text-xs font-semibold uppercase tracking-wide text-gray-300"
>
Send a zap
</h3>
<button
type="button"
id="modalZapCloseBtn"
class="text-gray-400 hover:text-gray-200 transition-colors"
aria-label="Close zap dialog"
>
✕
</button>
</div>
<form id="modalZapForm" class="space-y-3">
<div>
<label
for="modalZapAmountInput"
class="block text-xs font-semibold uppercase tracking-wide text-gray-300 mb-2"
>
Zap amount (sats)
</label>
<input
id="modalZapAmountInput"
type="number"
min="1"
step="1"
inputmode="numeric"
class="w-full rounded border border-gray-600 bg-gray-800 px-3 py-2 text-sm text-gray-100 focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400"
placeholder="Enter sats"
/>
</div>
<div>
<label
for="modalZapCommentInput"
class="block text-xs font-semibold uppercase tracking-wide text-gray-300 mb-2"
>
Comment (optional)
</label>
<textarea
id="modalZapCommentInput"
rows="2"
maxlength="280"
class="w-full resize-y rounded border border-gray-600 bg-gray-800 px-3 py-2 text-sm text-gray-100 focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400"
placeholder="Include a note with your zap"
></textarea>
</div>
<p
id="modalZapSplitSummary"
class="text-sm text-gray-300"
aria-live="polite"
>
Enter an amount to view the split.
</p>
<p
id="modalZapStatus"
class="text-xs text-gray-400"
role="status"
aria-live="polite"
></p>
<ul
id="modalZapReceipts"
class="space-y-2 text-xs text-gray-300"
></ul>
<div class="flex items-center justify-end gap-2">
<button
type="submit"
id="modalZapSendBtn"
class="rounded bg-blue-500 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-gray-900"
>
Send zap
</button>
</div>
</form>
</div>
</div>
<!-- Torrent stats -->
<div class="bg-gray-800/50 rounded-lg p-4">
<div class="w-full bg-gray-700 rounded-full h-2 mb-2">
+13
View File
@@ -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;
+33 -1
View File
@@ -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();
+161 -7
View File
@@ -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();
+32
View File
@@ -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 });
}
+90 -42
View File
@@ -29,18 +29,96 @@
</div>
<div class="flex flex-col items-end gap-3 self-start sm:self-end">
<div class="flex items-center gap-2">
<button
id="zapButton"
type="button"
class="icon-button"
aria-label="Send a zap"
>
<img
src="assets/svg/lightning-bolt.svg"
alt="Zap"
class="icon-image"
/>
</button>
<div class="relative" data-channel-zap-wrapper>
<button
id="zapButton"
type="button"
class="icon-button"
aria-label="Open zap dialog"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="zapControls"
>
<img
src="assets/svg/lightning-bolt.svg"
alt="Zap"
class="icon-image"
/>
</button>
<div
id="zapControls"
class="zap-popover hidden"
role="dialog"
aria-hidden="true"
aria-label="Channel zap dialog"
>
<div
class="zap-popover__content w-72 max-w-[calc(100vw-2rem)] rounded-lg bg-gray-900/95 p-4 shadow-lg backdrop-blur"
>
<div class="mb-3 flex items-center justify-between gap-4">
<h3
class="text-xs font-semibold uppercase tracking-wide text-gray-300"
>
Send a zap
</h3>
<button
type="button"
id="zapCloseBtn"
class="text-gray-400 transition-colors hover:text-gray-200"
aria-label="Close zap dialog"
>
✕
</button>
</div>
<p
id="zapWalletPrompt"
class="mb-3 hidden text-sm text-gray-200"
aria-hidden="true"
>
Connect a wallet in
<button
type="button"
id="zapWalletLink"
class="font-semibold text-blue-300 underline decoration-dotted transition-colors hover:text-blue-200"
>
Wallet Connect settings
</button>
to send zaps.
</p>
<label
for="zapAmountInput"
id="zapAmountLabel"
class="mb-2 block text-xs font-semibold uppercase tracking-wide text-gray-300"
>
Zap amount (sats)
</label>
<input
id="zapAmountInput"
type="number"
min="1"
step="1"
inputmode="numeric"
class="w-full rounded border border-gray-600 bg-gray-800 px-3 py-2 text-sm text-gray-100 focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400"
placeholder="Enter sats"
aria-labelledby="zapAmountLabel"
/>
<p
id="zapSplitSummary"
class="mt-3 text-sm text-gray-300"
aria-live="polite"
>
Enter an amount to view the split.
</p>
<p
id="zapStatus"
class="mt-2 text-xs text-gray-400"
role="status"
aria-live="polite"
></p>
<ul id="zapReceipts" class="mt-3 space-y-2 text-xs text-gray-300"></ul>
</div>
</div>
</div>
<button
id="channelShareBtn"
type="button"
@@ -108,36 +186,6 @@
</div>
</div>
</div>
<div id="zapControls" class="hidden w-full max-w-xs sm:max-w-sm">
<div class="w-full rounded-lg bg-gray-900 bg-opacity-70 p-4 shadow-lg backdrop-blur">
<label for="zapAmountInput" class="mb-2 block text-xs font-semibold uppercase tracking-wide text-gray-300">
Zap amount (sats)
</label>
<input
id="zapAmountInput"
type="number"
min="1"
step="1"
inputmode="numeric"
class="w-full rounded border border-gray-600 bg-gray-800 px-3 py-2 text-sm text-gray-100 focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400"
placeholder="Enter sats"
/>
<p
id="zapSplitSummary"
class="mt-3 text-sm text-gray-300"
aria-live="polite"
>
Enter an amount to view the split.
</p>
<p
id="zapStatus"
class="mt-2 text-xs text-gray-400"
role="status"
aria-live="polite"
></p>
<ul id="zapReceipts" class="mt-3 space-y-2 text-xs text-gray-300"></ul>
</div>
</div>
</div>
</div>
</div>