mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Merge pull request #2640 from PR0M3TH3AN/agents/daily/decompose-agent-14657238852522224687
[decompose] VideoModal decomposition
This commit is contained in:
6 files changed
+1375
-1107
No files matched your search
@@ -0,0 +1,73 @@
|
||||
# Decomposition Plan for `js/ui/components/VideoModal.js`
|
||||
|
||||
**Date:** 2026-02-20
|
||||
**Agent:** decompose-agent (via scheduler)
|
||||
**Target File:** `js/ui/components/VideoModal.js`
|
||||
**Current Size:** ~6000 lines (grandfathered)
|
||||
|
||||
## Rationale
|
||||
`VideoModal.js` is one of the largest files in the codebase. It handles too many responsibilities beyond just managing the modal lifecycle and video playback. Specifically, it contains extensive logic for:
|
||||
1. **Zap Dialog Management**: Handling the entire flow of zapping, including popover, form validation, receipts, and error handling.
|
||||
2. **Link Previews**: Fetching and rendering link previews for the video description.
|
||||
|
||||
Extracting these into dedicated controllers will significantly reduce the size of `VideoModal.js` and improve cohesion.
|
||||
|
||||
## Extraction Plan
|
||||
|
||||
### 1. `ZapController`
|
||||
**Source:** `js/ui/components/VideoModal.js`
|
||||
**Destination:** `js/ui/components/video-modal/zapController.js`
|
||||
|
||||
**Responsibilities:**
|
||||
- Manage the Zap dialog DOM elements (`modalZapDialog`, `modalZapForm`, etc.).
|
||||
- Handle Zap interactions (open, close, submit).
|
||||
- Render Zap receipts.
|
||||
- Manage Zap visibility and state.
|
||||
|
||||
**Methods to Move:**
|
||||
- `setupModalZapPopover`
|
||||
- `openZapDialog`
|
||||
- `closeZapDialog`
|
||||
- `isZapDialogOpen`
|
||||
- `focusZapAmount`
|
||||
- `getZapAmountValue`, `setZapAmount`
|
||||
- `getZapCommentValue`, `setZapComment`
|
||||
- `resetZapForm`
|
||||
- `setZapSplitSummary`
|
||||
- `setZapStatus`
|
||||
- `clearZapReceipts`, `renderZapReceipts`
|
||||
- `setZapPending`, `setZapRetryPending`, `setZapCompleted`
|
||||
- `applyZapSendButtonState`
|
||||
- `setZapVisibility`
|
||||
- `setWalletPromptVisible`
|
||||
|
||||
### 2. `LinkPreviewController`
|
||||
**Source:** `js/ui/components/VideoModal.js`
|
||||
**Destination:** `js/ui/components/video-modal/linkPreviewController.js`
|
||||
|
||||
**Responsibilities:**
|
||||
- Manage link preview DOM elements (`videoDescriptionPreviews`).
|
||||
- Fetch and render link previews.
|
||||
- Handle link preview settings.
|
||||
|
||||
**Methods to Move:**
|
||||
- `renderLinkPreviews`
|
||||
- `clearLinkPreviews`, `clearLinkPreviewRequests`
|
||||
- `extractDescriptionUrls`
|
||||
- `normalizePreviewUrl`, `extractPreviewDomain`
|
||||
- `createLinkPreviewCard`, `updateLinkPreviewCard`
|
||||
- `setLinkPreviewStatus`
|
||||
- `resolveLinkPreview`
|
||||
- `handleLinkPreviewSettingsChange`
|
||||
|
||||
## Implementation Details
|
||||
- `VideoModal` will instantiate these controllers in its constructor.
|
||||
- `hydrate()` will call `controller.initialize()`.
|
||||
- `destroy()` will call `controller.destroy()`.
|
||||
- `VideoModal` will delegate relevant calls to these controllers.
|
||||
- Existing event listeners in `VideoModal` that trigger these actions will be updated to use the controllers.
|
||||
|
||||
## Verification
|
||||
- `npm run lint`
|
||||
- `npm run test:unit`
|
||||
- Manual verification via code review (simulated).
|
||||
@@ -0,0 +1,27 @@
|
||||
# Task Completion Log: Decompose Agent
|
||||
|
||||
**Date:** 2026-02-20
|
||||
**Agent:** decompose-agent
|
||||
**Status:** Completed
|
||||
|
||||
## Summary
|
||||
Successfully decomposed `js/ui/components/VideoModal.js` by extracting distinct functional blocks into separate controllers. This improved code organization and reduced the file size significantly.
|
||||
|
||||
### Decomposition Details
|
||||
- **Source File:** `js/ui/components/VideoModal.js` (Lines before: 6045)
|
||||
- **Extracted Controllers:**
|
||||
1. `js/ui/components/video-modal/zapController.js`: Encapsulates all Zap dialog logic (popover, form, receipts, visibility).
|
||||
2. `js/ui/components/video-modal/linkPreviewController.js`: Encapsulates link preview fetching and rendering logic.
|
||||
- **Lines Reduced:** 1069 (New size: 4976 lines)
|
||||
|
||||
### Bug Fixes
|
||||
- Addressed code review feedback: Updated `ZapController` and `LinkPreviewController` to access `document` and `window` dynamically from the modal instance to avoid initialization timing issues.
|
||||
|
||||
### Verification
|
||||
- **Lint:** Passed (`npm run lint`).
|
||||
- **Unit Tests:** Passed (`npm run test:unit`).
|
||||
- **Baseline:** Updated `scripts/check-file-size.mjs` with new line count for `js/ui/components/VideoModal.js`.
|
||||
|
||||
### Artifacts
|
||||
- `context/CONTEXT_2026-02-20_decompose-agent.md`: Detailed plan and rationale.
|
||||
- New files: `js/ui/components/video-modal/zapController.js`, `js/ui/components/video-modal/linkPreviewController.js`.
|
||||
+37
-1106
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,309 @@
|
||||
import { LinkPreviewService } from "../../../services/linkPreviewService.js";
|
||||
import {
|
||||
allowLinkPreviewDomain,
|
||||
getLinkPreviewSettings,
|
||||
isLinkPreviewDomainAllowed,
|
||||
subscribeToLinkPreviewSettings,
|
||||
} from "../../../utils/linkPreviewSettings.js";
|
||||
import { devLogger } from "../../../utils/logger.js";
|
||||
|
||||
export class LinkPreviewController {
|
||||
constructor({ modal }) {
|
||||
this.modal = modal;
|
||||
this.linkPreviewService = new LinkPreviewService();
|
||||
this.linkPreviewAbortControllers = new Map();
|
||||
this.linkPreviewSettingsUnsubscribe = null;
|
||||
this.currentDescriptionText = "";
|
||||
this.videoDescriptionPreviews = null;
|
||||
|
||||
this.handleLinkPreviewSettingsChange =
|
||||
this.handleLinkPreviewSettingsChange.bind(this);
|
||||
}
|
||||
|
||||
get document() {
|
||||
return this.modal?.document || null;
|
||||
}
|
||||
|
||||
initialize({ playerModal }) {
|
||||
if (!playerModal) return;
|
||||
|
||||
this.videoDescriptionPreviews =
|
||||
playerModal.querySelector("#videoDescriptionPreviews") || null;
|
||||
|
||||
if (this.linkPreviewSettingsUnsubscribe) {
|
||||
this.linkPreviewSettingsUnsubscribe();
|
||||
}
|
||||
this.linkPreviewSettingsUnsubscribe = subscribeToLinkPreviewSettings(
|
||||
this.handleLinkPreviewSettingsChange,
|
||||
);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.clearLinkPreviews();
|
||||
if (this.linkPreviewSettingsUnsubscribe) {
|
||||
this.linkPreviewSettingsUnsubscribe();
|
||||
this.linkPreviewSettingsUnsubscribe = null;
|
||||
}
|
||||
this.videoDescriptionPreviews = null;
|
||||
this.linkPreviewAbortControllers.clear();
|
||||
}
|
||||
|
||||
handleLinkPreviewSettingsChange(event) {
|
||||
const detail = event?.detail?.settings || null;
|
||||
if (!this.videoDescriptionPreviews) {
|
||||
return;
|
||||
}
|
||||
const nextText = this.currentDescriptionText || "";
|
||||
if (!nextText) {
|
||||
return;
|
||||
}
|
||||
if (detail && typeof detail.autoFetchUnknownDomains === "boolean") {
|
||||
this.renderLinkPreviews(nextText);
|
||||
}
|
||||
}
|
||||
|
||||
clearLinkPreviewRequests() {
|
||||
if (!(this.linkPreviewAbortControllers instanceof Map)) {
|
||||
this.linkPreviewAbortControllers = new Map();
|
||||
return;
|
||||
}
|
||||
this.linkPreviewAbortControllers.forEach((controller) => {
|
||||
try {
|
||||
controller.abort();
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
});
|
||||
this.linkPreviewAbortControllers.clear();
|
||||
}
|
||||
|
||||
clearLinkPreviews() {
|
||||
this.clearLinkPreviewRequests();
|
||||
this.currentDescriptionText = "";
|
||||
if (this.videoDescriptionPreviews) {
|
||||
this.videoDescriptionPreviews.textContent = "";
|
||||
this.videoDescriptionPreviews.setAttribute("hidden", "");
|
||||
}
|
||||
}
|
||||
|
||||
renderLinkPreviews(description) {
|
||||
const root = this.videoDescriptionPreviews;
|
||||
if (!root || !this.document) {
|
||||
return;
|
||||
}
|
||||
const text =
|
||||
typeof description === "string" ? description : String(description ?? "");
|
||||
this.currentDescriptionText = text;
|
||||
this.clearLinkPreviewRequests();
|
||||
root.textContent = "";
|
||||
|
||||
const urls = this.extractDescriptionUrls(text);
|
||||
if (!urls.length) {
|
||||
root.setAttribute("hidden", "");
|
||||
return;
|
||||
}
|
||||
|
||||
root.removeAttribute("hidden");
|
||||
const fragment = this.document.createDocumentFragment();
|
||||
urls.forEach((url) => {
|
||||
const domain = this.extractPreviewDomain(url);
|
||||
const card = this.createLinkPreviewCard({ url, domain });
|
||||
fragment.appendChild(card);
|
||||
void this.resolveLinkPreview(url, domain, card);
|
||||
});
|
||||
root.appendChild(fragment);
|
||||
}
|
||||
|
||||
extractDescriptionUrls(text) {
|
||||
if (!text) {
|
||||
return [];
|
||||
}
|
||||
const urlPattern = /\bhttps?:\/\/[^\s<>"']+/gi;
|
||||
const urls = new Set();
|
||||
let match;
|
||||
while ((match = urlPattern.exec(text)) !== null) {
|
||||
const normalized = this.normalizePreviewUrl(match[0]);
|
||||
if (normalized) {
|
||||
urls.add(normalized);
|
||||
}
|
||||
}
|
||||
return Array.from(urls);
|
||||
}
|
||||
|
||||
normalizePreviewUrl(candidate) {
|
||||
if (!candidate) {
|
||||
return "";
|
||||
}
|
||||
let href = typeof candidate === "string" ? candidate : String(candidate ?? "");
|
||||
let trailing = "";
|
||||
const trailingPattern = /[)\]\}>"',.;!?]+$/;
|
||||
while (href && trailingPattern.test(href)) {
|
||||
trailing = href.slice(-1) + trailing;
|
||||
href = href.slice(0, -1);
|
||||
}
|
||||
return href.trim();
|
||||
}
|
||||
|
||||
extractPreviewDomain(url) {
|
||||
if (!url) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return new URL(url).hostname || "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
createLinkPreviewCard({ url, domain }) {
|
||||
const card = this.document.createElement("article");
|
||||
card.className = "card flex flex-col gap-2 p-3";
|
||||
card.dataset.linkPreviewUrl = url;
|
||||
|
||||
const header = this.document.createElement("div");
|
||||
header.className = "flex items-start gap-3";
|
||||
|
||||
const imageWrapper = this.document.createElement("div");
|
||||
imageWrapper.className =
|
||||
"flex h-16 w-16 flex-shrink-0 items-center justify-center overflow-hidden rounded-md border border-border/60 bg-surface-muted text-2xs text-muted";
|
||||
const image = this.document.createElement("img");
|
||||
image.hidden = true;
|
||||
image.loading = "lazy";
|
||||
image.decoding = "async";
|
||||
image.className = "h-full w-full object-cover";
|
||||
const imagePlaceholder = this.document.createElement("span");
|
||||
imagePlaceholder.textContent = "Preview";
|
||||
imageWrapper.appendChild(image);
|
||||
imageWrapper.appendChild(imagePlaceholder);
|
||||
|
||||
const content = this.document.createElement("div");
|
||||
content.className = "flex min-w-0 flex-1 flex-col gap-1";
|
||||
|
||||
const domainEl = this.document.createElement("p");
|
||||
domainEl.className = "text-3xs uppercase tracking-wide text-muted";
|
||||
domainEl.textContent = domain || "Link preview";
|
||||
|
||||
const titleEl = this.document.createElement("a");
|
||||
titleEl.className =
|
||||
"text-sm font-semibold text-text break-words hover:underline focus-ring";
|
||||
titleEl.href = url;
|
||||
titleEl.target = "_blank";
|
||||
titleEl.rel = "noopener noreferrer";
|
||||
titleEl.textContent = url;
|
||||
|
||||
const descEl = this.document.createElement("p");
|
||||
descEl.className = "text-xs text-muted";
|
||||
descEl.textContent = "";
|
||||
|
||||
content.appendChild(domainEl);
|
||||
content.appendChild(titleEl);
|
||||
content.appendChild(descEl);
|
||||
|
||||
header.appendChild(imageWrapper);
|
||||
header.appendChild(content);
|
||||
|
||||
const statusEl = this.document.createElement("p");
|
||||
statusEl.className = "text-xs text-muted";
|
||||
|
||||
const actions = this.document.createElement("div");
|
||||
actions.className = "flex flex-wrap gap-2";
|
||||
|
||||
card.appendChild(header);
|
||||
card.appendChild(statusEl);
|
||||
card.appendChild(actions);
|
||||
|
||||
card.__linkPreviewElements = {
|
||||
imageWrapper,
|
||||
image,
|
||||
imagePlaceholder,
|
||||
domainEl,
|
||||
titleEl,
|
||||
descEl,
|
||||
statusEl,
|
||||
actions,
|
||||
};
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
setLinkPreviewStatus(card, { message = "", actionLabel = "", onAction } = {}) {
|
||||
const elements = card?.__linkPreviewElements;
|
||||
if (!elements) {
|
||||
return;
|
||||
}
|
||||
const statusText = typeof message === "string" ? message : "";
|
||||
elements.statusEl.textContent = statusText;
|
||||
elements.actions.textContent = "";
|
||||
|
||||
if (actionLabel && typeof onAction === "function") {
|
||||
const button = this.document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "btn-ghost focus-ring text-xs";
|
||||
button.textContent = actionLabel;
|
||||
button.addEventListener("click", onAction);
|
||||
elements.actions.appendChild(button);
|
||||
}
|
||||
}
|
||||
|
||||
updateLinkPreviewCard(card, preview, { domain } = {}) {
|
||||
const elements = card?.__linkPreviewElements;
|
||||
if (!elements || !preview) {
|
||||
return;
|
||||
}
|
||||
const title = preview.title || preview.siteName || preview.url || "";
|
||||
elements.titleEl.textContent = title || preview.url;
|
||||
elements.descEl.textContent = preview.description || "";
|
||||
const label = preview.siteName || domain || "Link preview";
|
||||
elements.domainEl.textContent = label;
|
||||
|
||||
if (preview.image) {
|
||||
elements.image.src = preview.image;
|
||||
elements.image.alt = title || "Link preview";
|
||||
elements.image.hidden = false;
|
||||
elements.imagePlaceholder.hidden = true;
|
||||
} else {
|
||||
elements.image.hidden = true;
|
||||
elements.imagePlaceholder.hidden = false;
|
||||
}
|
||||
|
||||
this.setLinkPreviewStatus(card, { message: "" });
|
||||
}
|
||||
|
||||
async resolveLinkPreview(url, domain, card) {
|
||||
const settings = getLinkPreviewSettings();
|
||||
const isAllowed =
|
||||
settings.autoFetchUnknownDomains ||
|
||||
isLinkPreviewDomainAllowed(domain, settings);
|
||||
|
||||
if (!isAllowed) {
|
||||
this.setLinkPreviewStatus(card, {
|
||||
message: "Preview disabled for new domains.",
|
||||
actionLabel: domain ? `Allow previews for ${domain}` : "Allow previews",
|
||||
onAction: () => {
|
||||
if (domain) {
|
||||
allowLinkPreviewDomain(domain);
|
||||
}
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setLinkPreviewStatus(card, { message: "Loading preview…" });
|
||||
const controller = new AbortController();
|
||||
this.linkPreviewAbortControllers.set(url, controller);
|
||||
const preview = await this.linkPreviewService.getPreview(url, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
this.linkPreviewAbortControllers.delete(url);
|
||||
|
||||
if (!preview) {
|
||||
this.setLinkPreviewStatus(card, { message: "Preview unavailable." });
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateLinkPreviewCard(card, preview, { domain });
|
||||
if (domain) {
|
||||
allowLinkPreviewDomain(domain, { silent: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,928 @@
|
||||
import createPopover from "../../overlay/popoverEngine.js";
|
||||
import { devLogger } from "../../../utils/logger.js";
|
||||
|
||||
export class ZapController {
|
||||
constructor({ modal }) {
|
||||
this.modal = modal;
|
||||
|
||||
// DOM Elements
|
||||
this.modalZapDialog = null;
|
||||
this.modalZapForm = null;
|
||||
this.modalZapAmountInput = null;
|
||||
this.modalZapCommentInput = null;
|
||||
this.modalZapSplitSummary = null;
|
||||
this.modalZapStatusEl = null;
|
||||
this.modalZapReceipts = null;
|
||||
this.modalZapSendBtn = null;
|
||||
this.modalZapCloseBtn = null;
|
||||
this.modalZapWalletPrompt = null;
|
||||
this.modalZapWalletLink = null;
|
||||
this.modalZapBtn = null;
|
||||
|
||||
// State
|
||||
this.modalZapDialogOpen = false;
|
||||
this.modalZapPending = false;
|
||||
this.modalZapRequiresLogin = false;
|
||||
this.modalZapPopover = null;
|
||||
this.modalZapOpenPromise = null;
|
||||
this.modalZapPendingToggle = null;
|
||||
}
|
||||
|
||||
get document() {
|
||||
return this.modal?.document || null;
|
||||
}
|
||||
|
||||
get window() {
|
||||
return this.modal?.window || null;
|
||||
}
|
||||
|
||||
initialize({ playerModal }) {
|
||||
if (!playerModal) return;
|
||||
|
||||
this.modalZapDialog = playerModal.querySelector("#modalZapDialog") || null;
|
||||
this.modalZapForm = playerModal.querySelector("#modalZapForm") || null;
|
||||
this.modalZapAmountInput =
|
||||
playerModal.querySelector("#modalZapAmountInput") || null;
|
||||
this.modalZapCommentInput =
|
||||
playerModal.querySelector("#modalZapCommentInput") || null;
|
||||
this.modalZapSplitSummary =
|
||||
playerModal.querySelector("#modalZapSplitSummary") || null;
|
||||
this.modalZapStatusEl =
|
||||
playerModal.querySelector("#modalZapStatus") || null;
|
||||
this.modalZapReceipts =
|
||||
playerModal.querySelector("#modalZapReceipts") || null;
|
||||
this.modalZapSendBtn =
|
||||
playerModal.querySelector("#modalZapSendBtn") || null;
|
||||
this.modalZapCloseBtn =
|
||||
playerModal.querySelector("#modalZapCloseBtn") || null;
|
||||
this.modalZapWalletPrompt =
|
||||
playerModal.querySelector("#modalZapWalletPrompt") || null;
|
||||
this.modalZapWalletLink =
|
||||
playerModal.querySelector("#modalZapWalletLink") || null;
|
||||
this.modalZapBtn = playerModal.querySelector("#modalZapBtn") || null;
|
||||
|
||||
this.modalZapDialogOpen = false;
|
||||
this.setupModalZapPopover();
|
||||
|
||||
this.bindEvents();
|
||||
this.setZapVisibility(false);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.modalZapPopover?.destroy) {
|
||||
this.modalZapPopover.destroy();
|
||||
}
|
||||
this.modalZapPopover = null;
|
||||
this.modalZapOpenPromise = null;
|
||||
this.modalZapPendingToggle = null;
|
||||
|
||||
// Clear references
|
||||
this.modalZapDialog = null;
|
||||
this.modalZapForm = null;
|
||||
this.modalZapAmountInput = null;
|
||||
this.modalZapCommentInput = null;
|
||||
this.modalZapSplitSummary = null;
|
||||
this.modalZapStatusEl = null;
|
||||
this.modalZapReceipts = null;
|
||||
this.modalZapSendBtn = null;
|
||||
this.modalZapCloseBtn = null;
|
||||
this.modalZapWalletPrompt = null;
|
||||
this.modalZapWalletLink = null;
|
||||
this.modalZapBtn = null;
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
if (this.modalZapBtn) {
|
||||
this.modalZapBtn.addEventListener("click", (event) => {
|
||||
event?.preventDefault?.();
|
||||
event?.stopPropagation?.();
|
||||
|
||||
if (this.modalZapBtn?.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modalZapRequiresLogin) {
|
||||
this.modal.dispatch(
|
||||
"zap:open",
|
||||
{
|
||||
video: this.modal.activeVideo,
|
||||
requiresLogin: true,
|
||||
},
|
||||
{ cancelable: false },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const popoverIsOpen = this.isZapDialogOpen();
|
||||
|
||||
if (this.modalZapOpenPromise) {
|
||||
this.modalZapPendingToggle = "close";
|
||||
this.closeZapDialog({ silent: true, restoreFocus: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (popoverIsOpen) {
|
||||
if (this.modalZapPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeZapDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
const allowed = this.modal.dispatch(
|
||||
"zap:open",
|
||||
{ video: this.modal.activeVideo },
|
||||
{ cancelable: true },
|
||||
);
|
||||
if (allowed === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.modalZapPendingToggle = null;
|
||||
this.openZapDialog();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.modalZapCloseBtn) {
|
||||
this.modalZapCloseBtn.addEventListener("click", (event) => {
|
||||
event?.preventDefault?.();
|
||||
this.closeZapDialog();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.modalZapWalletLink) {
|
||||
this.modalZapWalletLink.addEventListener("click", (event) => {
|
||||
event?.preventDefault?.();
|
||||
this.modal.dispatch("zap:wallet-link", { video: this.modal.activeVideo });
|
||||
});
|
||||
}
|
||||
|
||||
if (this.modalZapForm) {
|
||||
this.modalZapForm.addEventListener("submit", (event) => {
|
||||
event?.preventDefault?.();
|
||||
if (this.modalZapSendBtn?.dataset.completed === "true") {
|
||||
this.closeZapDialog();
|
||||
return;
|
||||
}
|
||||
if (this.modalZapSendBtn?.disabled) {
|
||||
return;
|
||||
}
|
||||
this.modal.dispatch("video:zap", {
|
||||
video: this.modal.activeVideo,
|
||||
amount: this.getZapAmountValue(),
|
||||
comment: this.getZapCommentValue()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (this.modalZapSendBtn) {
|
||||
this.modalZapSendBtn.addEventListener("click", (event) => {
|
||||
if (this.modalZapSendBtn?.dataset.completed === "true") {
|
||||
event?.preventDefault?.();
|
||||
this.closeZapDialog();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.modalZapAmountInput) {
|
||||
const amountHandler = () => {
|
||||
this.modal.dispatch("zap:amount-change", {
|
||||
video: this.modal.activeVideo,
|
||||
amount: this.getZapAmountValue()
|
||||
});
|
||||
};
|
||||
this.modalZapAmountInput.addEventListener("input", amountHandler);
|
||||
this.modalZapAmountInput.addEventListener("change", amountHandler);
|
||||
}
|
||||
|
||||
if (this.modalZapCommentInput) {
|
||||
const commentHandler = () => {
|
||||
this.modal.dispatch("zap:comment-change", {
|
||||
video: this.modal.activeVideo,
|
||||
comment: this.getZapCommentValue()
|
||||
});
|
||||
};
|
||||
this.modalZapCommentInput.addEventListener("input", commentHandler);
|
||||
}
|
||||
}
|
||||
|
||||
setupModalZapPopover() {
|
||||
if (!this.modalZapDialog) {
|
||||
this.modalZapPopover = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.modalZapDialog.dataset.state) {
|
||||
const isHidden =
|
||||
this.modalZapDialog.hasAttribute("hidden") ||
|
||||
this.modalZapDialog.getAttribute("aria-hidden") === "true";
|
||||
this.modalZapDialog.dataset.state = isHidden ? "closed" : "open";
|
||||
}
|
||||
|
||||
if (this.modalZapDialog.dataset.state !== "open") {
|
||||
this.modalZapDialog.hidden = true;
|
||||
this.modalZapDialog.setAttribute("aria-hidden", "true");
|
||||
this.modalZapDialogOpen = false;
|
||||
}
|
||||
|
||||
if (!this.modalZapBtn) {
|
||||
this.modalZapPopover = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const documentRef =
|
||||
this.modalZapDialog.ownerDocument ||
|
||||
this.modalZapBtn.ownerDocument ||
|
||||
this.document ||
|
||||
(typeof document !== "undefined" ? document : null);
|
||||
|
||||
const popover = createPopover(
|
||||
this.modalZapBtn,
|
||||
() => this.modalZapDialog,
|
||||
{
|
||||
document: documentRef,
|
||||
placement: "bottom-end",
|
||||
restoreFocusOnClose: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (!popover) {
|
||||
this.modalZapPopover = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const originalOpen = popover.open?.bind(popover);
|
||||
if (originalOpen) {
|
||||
popover.open = async (...args) => {
|
||||
const result = await originalOpen(...args);
|
||||
if (result) {
|
||||
this.modalZapDialog.dataset.state = "open";
|
||||
this.modalZapDialog.hidden = false;
|
||||
this.modalZapDialog.setAttribute("aria-hidden", "false");
|
||||
this.modalZapDialogOpen = true;
|
||||
if (this.modalZapBtn) {
|
||||
this.modalZapBtn.setAttribute("aria-expanded", "true");
|
||||
}
|
||||
this.focusZapAmount();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
const originalClose = popover.close?.bind(popover);
|
||||
if (originalClose) {
|
||||
popover.close = (options = {}) => {
|
||||
const { silent = false, ...rest } = options;
|
||||
const wasOpen = popover.isOpen?.() === true;
|
||||
const result = originalClose(rest);
|
||||
const wasDialogMarkedOpen =
|
||||
this.modalZapDialogOpen === true ||
|
||||
this.modalZapDialog?.dataset?.state === "open";
|
||||
if (wasOpen || wasDialogMarkedOpen) {
|
||||
this.modalZapPendingToggle = null;
|
||||
this.modalZapOpenPromise = null;
|
||||
this.modalZapDialog.dataset.state = "closed";
|
||||
this.modalZapDialog.setAttribute("aria-hidden", "true");
|
||||
this.modalZapDialog.hidden = true;
|
||||
this.modalZapDialogOpen = false;
|
||||
if (this.modalZapBtn) {
|
||||
this.modalZapBtn.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
if (!silent && (wasOpen || wasDialogMarkedOpen)) {
|
||||
this.modal.dispatch("zap:close", { video: this.modal.activeVideo });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
const originalDestroy = popover.destroy?.bind(popover);
|
||||
if (originalDestroy) {
|
||||
popover.destroy = (...args) => {
|
||||
originalDestroy(...args);
|
||||
if (this.modalZapPopover === popover) {
|
||||
this.modalZapPopover = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.modalZapPopover = popover;
|
||||
}
|
||||
|
||||
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 && !requiresLogin);
|
||||
this.modalZapBtn.disabled = disableButton;
|
||||
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) {
|
||||
this.modalZapBtn.removeAttribute("tabindex");
|
||||
} else {
|
||||
this.modalZapBtn.setAttribute("tabindex", "-1");
|
||||
}
|
||||
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 || requiresLogin) {
|
||||
this.closeZapDialog({ silent: true, restoreFocus: false });
|
||||
}
|
||||
}
|
||||
|
||||
setWalletPromptVisible(visible) {
|
||||
if (!this.modalZapWalletPrompt) {
|
||||
return;
|
||||
}
|
||||
const shouldShow = !!visible;
|
||||
this.modalZapWalletPrompt.toggleAttribute("hidden", !shouldShow);
|
||||
this.modalZapWalletPrompt.setAttribute(
|
||||
"aria-hidden",
|
||||
(!shouldShow).toString()
|
||||
);
|
||||
}
|
||||
|
||||
async openZapDialog() {
|
||||
if (this.modalZapRequiresLogin) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
if (this.modalZapOpenPromise) {
|
||||
return this.modalZapOpenPromise;
|
||||
}
|
||||
|
||||
const runOpen = async () => {
|
||||
if (this.modalZapPopover?.open) {
|
||||
const opened = await this.modalZapPopover.open();
|
||||
if (opened) {
|
||||
this.modalZapDialogOpen = true;
|
||||
if (this.modalZapDialog) {
|
||||
this.modalZapDialog.dataset.state = "open";
|
||||
this.modalZapDialog.hidden = false;
|
||||
this.modalZapDialog.setAttribute("aria-hidden", "false");
|
||||
}
|
||||
this.focusZapAmount();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.modalZapDialog) {
|
||||
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;
|
||||
}
|
||||
|
||||
return opened;
|
||||
}
|
||||
|
||||
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();
|
||||
return true;
|
||||
};
|
||||
|
||||
const promise = runOpen().catch((error) => {
|
||||
this.log("[ZapController] 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 } = {}) {
|
||||
const dialogAppearsOpen = this.isZapDialogOpen();
|
||||
|
||||
if (this.modalZapOpenPromise && !dialogAppearsOpen) {
|
||||
this.modalZapPendingToggle = "close";
|
||||
return;
|
||||
}
|
||||
|
||||
this.modalZapPendingToggle = null;
|
||||
if (this.modalZapPopover?.close) {
|
||||
const options = { silent };
|
||||
if (restoreFocus !== undefined) {
|
||||
options.restoreFocus = restoreFocus;
|
||||
}
|
||||
const closeResult = this.modalZapPopover.close(options);
|
||||
|
||||
if (
|
||||
closeResult !== true &&
|
||||
this.modalZapDialog &&
|
||||
this.modalZapDialogOpen
|
||||
) {
|
||||
this.modalZapDialog.dataset.state = "closed";
|
||||
this.modalZapDialog.setAttribute("aria-hidden", "true");
|
||||
this.modalZapDialog.hidden = true;
|
||||
this.modalZapDialogOpen = false;
|
||||
if (this.modalZapBtn) {
|
||||
this.modalZapBtn.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
if (!silent) {
|
||||
this.modal.dispatch("zap:close", { video: this.modal.activeVideo });
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.modalZapDialog) {
|
||||
return;
|
||||
}
|
||||
if (this.modalZapDialogOpen) {
|
||||
this.modalZapDialog.dataset.state = "closed";
|
||||
this.modalZapDialog.setAttribute("aria-hidden", "true");
|
||||
this.modalZapDialog.hidden = true;
|
||||
this.modalZapDialogOpen = false;
|
||||
if (this.modalZapBtn) {
|
||||
this.modalZapBtn.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
if (!silent) {
|
||||
this.modal.dispatch("zap:close", { video: this.modal.activeVideo });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isZapDialogOpen() {
|
||||
const popoverIsOpen =
|
||||
typeof this.modalZapPopover?.isOpen === "function"
|
||||
? this.modalZapPopover.isOpen()
|
||||
: null;
|
||||
|
||||
if (popoverIsOpen === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (popoverIsOpen === false && this.modalZapDialogOpen) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.modalZapDialog?.dataset?.state === "open") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.modalZapDialog && this.modalZapDialog.hidden === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !!this.modalZapDialogOpen;
|
||||
}
|
||||
|
||||
focusZapAmount() {
|
||||
if (
|
||||
this.modalZapAmountInput &&
|
||||
typeof this.modalZapAmountInput.focus === "function"
|
||||
) {
|
||||
this.modalZapAmountInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
getZapAmountValue() {
|
||||
if (!this.modalZapAmountInput) {
|
||||
return 0;
|
||||
}
|
||||
const numeric = Number(this.modalZapAmountInput.value);
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, Math.round(numeric));
|
||||
}
|
||||
|
||||
setZapAmount(value) {
|
||||
if (!this.modalZapAmountInput) {
|
||||
return;
|
||||
}
|
||||
if (value === null || value === undefined || value === "") {
|
||||
this.modalZapAmountInput.value = "";
|
||||
return;
|
||||
}
|
||||
const numeric = Number(value);
|
||||
if (Number.isFinite(numeric)) {
|
||||
this.modalZapAmountInput.value = Math.max(0, Math.round(numeric));
|
||||
return;
|
||||
}
|
||||
this.modalZapAmountInput.value = value;
|
||||
}
|
||||
|
||||
getZapCommentValue() {
|
||||
if (!this.modalZapCommentInput) {
|
||||
return "";
|
||||
}
|
||||
return (this.modalZapCommentInput.value || "").trim();
|
||||
}
|
||||
|
||||
setZapComment(value) {
|
||||
if (!this.modalZapCommentInput) {
|
||||
return;
|
||||
}
|
||||
this.modalZapCommentInput.value = typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
resetZapForm({ amount = "", comment = "" } = {}) {
|
||||
this.setZapAmount(amount);
|
||||
this.setZapComment(comment);
|
||||
this.setZapStatus("", "neutral");
|
||||
this.clearZapReceipts();
|
||||
this.setZapRetryPending(false);
|
||||
this.setZapCompleted(false);
|
||||
}
|
||||
|
||||
setZapSplitSummary(text) {
|
||||
if (!this.modalZapSplitSummary) {
|
||||
return;
|
||||
}
|
||||
const message = typeof text === "string" ? text : "";
|
||||
this.modalZapSplitSummary.textContent =
|
||||
message || "Enter an amount to view the split.";
|
||||
}
|
||||
|
||||
setZapStatus(message, tone = "neutral") {
|
||||
if (!this.modalZapStatusEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedTone = typeof tone === "string" ? tone : "neutral";
|
||||
const text = typeof message === "string" ? message : "";
|
||||
this.modalZapStatusEl.textContent = text;
|
||||
this.modalZapStatusEl.classList.remove(
|
||||
"text-text",
|
||||
"text-muted",
|
||||
"text-info",
|
||||
"text-critical",
|
||||
"text-warning-strong"
|
||||
);
|
||||
|
||||
if (!text) {
|
||||
this.modalZapStatusEl.classList.add("text-muted");
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalizedTone === "success") {
|
||||
this.modalZapStatusEl.classList.add("text-info");
|
||||
} else if (normalizedTone === "error") {
|
||||
this.modalZapStatusEl.classList.add("text-critical");
|
||||
} else if (normalizedTone === "warning") {
|
||||
this.modalZapStatusEl.classList.add("text-warning-strong");
|
||||
} else {
|
||||
this.modalZapStatusEl.classList.add("text-text");
|
||||
}
|
||||
}
|
||||
|
||||
clearZapReceipts() {
|
||||
if (!this.modalZapReceipts) {
|
||||
return;
|
||||
}
|
||||
while (this.modalZapReceipts.firstChild) {
|
||||
this.modalZapReceipts.removeChild(this.modalZapReceipts.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
renderZapReceipts(receipts = [], { partial = false } = {}) {
|
||||
if (!this.modalZapReceipts || !this.document) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearZapReceipts();
|
||||
|
||||
if (!Array.isArray(receipts) || receipts.length === 0) {
|
||||
if (partial) {
|
||||
const empty = this.document.createElement("li");
|
||||
empty.className = "text-sm text-text";
|
||||
empty.textContent = "No zap receipts available.";
|
||||
this.modalZapReceipts.appendChild(empty);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = receipts.filter(
|
||||
(receipt) => receipt && typeof receipt === "object"
|
||||
);
|
||||
|
||||
const validatedReceipts = normalized.filter(
|
||||
(receipt) => receipt?.validation?.status === "passed" && receipt.validation.event
|
||||
);
|
||||
|
||||
const paymentFailures = normalized.filter((receipt) => {
|
||||
if (!receipt) {
|
||||
return false;
|
||||
}
|
||||
const status =
|
||||
typeof receipt.status === "string" ? receipt.status.toLowerCase() : "";
|
||||
return status && status !== "success";
|
||||
});
|
||||
|
||||
const unvalidatedReceipts = normalized.filter((receipt) => {
|
||||
if (!receipt) {
|
||||
return false;
|
||||
}
|
||||
const status =
|
||||
typeof receipt.status === "string" ? receipt.status.toLowerCase() : "success";
|
||||
if (status !== "success" && status !== "") {
|
||||
return false;
|
||||
}
|
||||
const validationStatus =
|
||||
typeof receipt.validation?.status === "string"
|
||||
? receipt.validation.status.toLowerCase()
|
||||
: "";
|
||||
if (!validationStatus || validationStatus === "skipped") {
|
||||
return false;
|
||||
}
|
||||
return validationStatus !== "passed";
|
||||
});
|
||||
|
||||
const renderReceiptItem = ({ receipt, tone }) => {
|
||||
const li = this.document.createElement("li");
|
||||
li.className = "rounded border border-border p-3 bg-overlay-panel-soft";
|
||||
|
||||
const header = this.document.createElement("div");
|
||||
header.className =
|
||||
"flex items-center justify-between gap-2 text-xs text-text";
|
||||
|
||||
const shareType = receipt.recipientType || receipt.type || "creator";
|
||||
const shareLabel = this.document.createElement("span");
|
||||
const isPlatformShare = shareType === "platform";
|
||||
const label = isPlatformShare
|
||||
? "Platform fee"
|
||||
: shareType === "creator"
|
||||
? "Creator"
|
||||
: "Lightning";
|
||||
shareLabel.textContent = `${label} • ${Math.max(
|
||||
0,
|
||||
Math.round(Number(receipt.amount || 0))
|
||||
)} sats`;
|
||||
|
||||
const status = this.document.createElement("span");
|
||||
if (tone === "validated") {
|
||||
status.textContent = "Validated";
|
||||
status.className = "text-info";
|
||||
} else if (tone === "failed") {
|
||||
status.textContent = "Failed";
|
||||
status.className = "text-critical";
|
||||
} else {
|
||||
status.textContent = "Pending";
|
||||
status.className = "text-muted";
|
||||
}
|
||||
|
||||
header.appendChild(shareLabel);
|
||||
header.appendChild(status);
|
||||
li.appendChild(header);
|
||||
|
||||
const address = this.document.createElement("p");
|
||||
address.className = "mt-1 text-xs text-text break-all";
|
||||
if (receipt.address && !isPlatformShare) {
|
||||
address.textContent = receipt.address;
|
||||
li.appendChild(address);
|
||||
}
|
||||
|
||||
const detail = this.document.createElement("p");
|
||||
detail.className = "mt-2 text-xs text-muted";
|
||||
if (tone === "failed") {
|
||||
const errorMessage =
|
||||
(receipt.error && receipt.error.message) ||
|
||||
(typeof receipt.error === "string"
|
||||
? receipt.error
|
||||
: "Payment failed.");
|
||||
detail.textContent = errorMessage;
|
||||
} else {
|
||||
let detailMessage = "Invoice settled.";
|
||||
const preimage = receipt.payment?.result?.preimage;
|
||||
if (typeof preimage === "string" && preimage) {
|
||||
detailMessage = `Preimage: ${preimage.slice(0, 18)}${
|
||||
preimage.length > 18 ? "…" : ""
|
||||
}`;
|
||||
}
|
||||
detail.textContent = detailMessage;
|
||||
}
|
||||
li.appendChild(detail);
|
||||
|
||||
this.modalZapReceipts.appendChild(li);
|
||||
};
|
||||
|
||||
validatedReceipts.forEach((receipt) => {
|
||||
renderReceiptItem({ receipt, tone: "validated" });
|
||||
});
|
||||
|
||||
paymentFailures.forEach((receipt) => {
|
||||
renderReceiptItem({ receipt, tone: "failed" });
|
||||
});
|
||||
|
||||
if (!validatedReceipts.length && !paymentFailures.length && !unvalidatedReceipts.length) {
|
||||
const empty = this.document.createElement("li");
|
||||
empty.className = "text-sm text-text";
|
||||
empty.textContent = partial
|
||||
? "No zap receipts available."
|
||||
: "No zap receipts published yet.";
|
||||
this.modalZapReceipts.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
if (unvalidatedReceipts.length) {
|
||||
const warning = this.document.createElement("li");
|
||||
warning.className =
|
||||
"rounded border border-border p-3 bg-overlay-panel-soft text-xs text-warning-strong";
|
||||
const summaries = unvalidatedReceipts.map((receipt) => {
|
||||
const shareType = receipt.recipientType || receipt.type || "creator";
|
||||
const label =
|
||||
shareType === "platform"
|
||||
? "Platform"
|
||||
: shareType === "creator"
|
||||
? "Creator"
|
||||
: "Lightning";
|
||||
const address =
|
||||
typeof receipt.address === "string" && receipt.address
|
||||
? ` (${receipt.address})`
|
||||
: "";
|
||||
const reason =
|
||||
typeof receipt.validation?.reason === "string" && receipt.validation.reason
|
||||
? ` — ${receipt.validation.reason}`
|
||||
: " — Awaiting compliant receipt.";
|
||||
return `${label}${address}${reason}`;
|
||||
});
|
||||
|
||||
const intro = validatedReceipts.length
|
||||
? "Awaiting validated zap receipt for remaining share(s)."
|
||||
: "No validated zap receipts yet.";
|
||||
warning.textContent = `${intro} ${summaries.join(" ")}`.trim();
|
||||
this.modalZapReceipts.appendChild(warning);
|
||||
}
|
||||
}
|
||||
|
||||
setZapPending(pending) {
|
||||
const isPending = !!pending;
|
||||
this.modalZapPending = isPending;
|
||||
|
||||
if (this.modalZapSendBtn) {
|
||||
this.modalZapSendBtn.disabled = isPending;
|
||||
this.modalZapSendBtn.setAttribute(
|
||||
"aria-busy",
|
||||
isPending ? "true" : "false"
|
||||
);
|
||||
this.modalZapSendBtn.classList.toggle("opacity-50", isPending);
|
||||
this.modalZapSendBtn.classList.toggle("pointer-events-none", isPending);
|
||||
}
|
||||
|
||||
if (this.modalZapAmountInput) {
|
||||
this.modalZapAmountInput.disabled = isPending;
|
||||
}
|
||||
|
||||
if (this.modalZapCommentInput) {
|
||||
this.modalZapCommentInput.disabled = isPending;
|
||||
}
|
||||
|
||||
if (this.modalZapCloseBtn) {
|
||||
this.modalZapCloseBtn.disabled = isPending;
|
||||
this.modalZapCloseBtn.classList.toggle("opacity-50", isPending);
|
||||
this.modalZapCloseBtn.classList.toggle("pointer-events-none", isPending);
|
||||
}
|
||||
|
||||
if (this.modalZapBtn) {
|
||||
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 (!buttonHidden) {
|
||||
this.modalZapBtn.disabled = false;
|
||||
this.modalZapBtn.removeAttribute("aria-busy");
|
||||
this.modalZapBtn.classList.remove("opacity-50", "pointer-events-none");
|
||||
} else {
|
||||
this.modalZapBtn.removeAttribute("aria-busy");
|
||||
this.modalZapBtn.classList.remove("opacity-50", "pointer-events-none");
|
||||
this.modalZapBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setZapRetryPending(pending, { summary = "" } = {}) {
|
||||
if (!this.modalZapSendBtn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pending) {
|
||||
delete this.modalZapSendBtn.dataset.completed;
|
||||
this.modalZapSendBtn.dataset.retryPending = "true";
|
||||
if (summary) {
|
||||
this.modalZapSendBtn.dataset.retrySummary = summary;
|
||||
} else {
|
||||
delete this.modalZapSendBtn.dataset.retrySummary;
|
||||
}
|
||||
} else {
|
||||
delete this.modalZapSendBtn.dataset.retryPending;
|
||||
delete this.modalZapSendBtn.dataset.retrySummary;
|
||||
}
|
||||
|
||||
this.applyZapSendButtonState();
|
||||
}
|
||||
|
||||
setZapCompleted(completed) {
|
||||
if (!this.modalZapSendBtn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (completed) {
|
||||
delete this.modalZapSendBtn.dataset.retryPending;
|
||||
delete this.modalZapSendBtn.dataset.retrySummary;
|
||||
this.modalZapSendBtn.dataset.completed = "true";
|
||||
} else {
|
||||
delete this.modalZapSendBtn.dataset.completed;
|
||||
}
|
||||
|
||||
this.applyZapSendButtonState();
|
||||
}
|
||||
|
||||
applyZapSendButtonState() {
|
||||
if (!this.modalZapSendBtn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modalZapSendBtn.dataset.completed === "true") {
|
||||
this.modalZapSendBtn.textContent = "Done";
|
||||
this.modalZapSendBtn.setAttribute("aria-label", "Close zap dialog");
|
||||
this.modalZapSendBtn.title = "Close zap dialog";
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modalZapSendBtn.dataset.retryPending === "true") {
|
||||
this.modalZapSendBtn.textContent = "Retry";
|
||||
this.modalZapSendBtn.setAttribute(
|
||||
"aria-label",
|
||||
"Retry failed zap shares"
|
||||
);
|
||||
const summary = this.modalZapSendBtn.dataset.retrySummary;
|
||||
if (summary) {
|
||||
this.modalZapSendBtn.title = summary;
|
||||
} else {
|
||||
this.modalZapSendBtn.removeAttribute("title");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.modalZapSendBtn.textContent = "Send";
|
||||
this.modalZapSendBtn.setAttribute("aria-label", "Send a zap");
|
||||
this.modalZapSendBtn.removeAttribute("title");
|
||||
}
|
||||
|
||||
log(message, ...args) {
|
||||
if (this.modal.logger && typeof this.modal.logger.log === "function") {
|
||||
this.modal.logger.log(message, ...args);
|
||||
return;
|
||||
}
|
||||
devLogger.log(message, ...args);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ const GROWTH_MARGIN = 50; // lines — allowlisted files may grow by this much f
|
||||
// They must NOT grow beyond (recorded + GROWTH_MARGIN).
|
||||
const GRANDFATHERED = {
|
||||
"js/ui/profileModalController.js": 8130,
|
||||
"js/ui/components/VideoModal.js": 6045,
|
||||
"js/ui/components/VideoModal.js": 4976,
|
||||
"js/app.js": 5602,
|
||||
"js/channelProfile.js": 5529,
|
||||
"js/nostr/client.js": 4356,
|
||||
|
||||
Reference in new issue
Block a user