Refactor channel profile popovers

This commit is contained in:
thePR0M3TH3AN committed 2025-10-16 12:41:28 -04:00
1 parent 6f9f239144
commit 62a2694c23
3 files changed
+654 -346

No files matched your search

+580 -173
View File
@@ -11,13 +11,9 @@ import { attachUrlHealthBadges } from "./urlHealthObserver.js";
import { accessControl } from "./accessControl.js";
import { getApplication } from "./applicationContext.js";
import { escapeHTML } from "./utils/domUtils.js";
import positionFloatingPanel from "./ui/utils/positionFloatingPanel.js";
import { createFloatingPanelStyles } from "./ui/utils/floatingPanelStyles.js";
import {
getPopupOffsetPx,
getPopupViewportPaddingPx,
} from "./designSystem/metrics.js";
import createPopover from "./ui/overlay/popoverEngine.js";
import { VideoCard } from "./ui/components/VideoCard.js";
import { createChannelProfileMenuPanel } from "./ui/components/videoMenuRenderers.js";
import { ALLOW_NSFW_CONTENT } from "./config.js";
import {
calculateZapShares,
@@ -131,7 +127,10 @@ let cachedZapSendBtn = null;
let pendingZapRetry = null;
let zapInFlight = false;
let zapControlsOpen = false;
let zapControlsPositioner = null;
let zapPopover = null;
let zapShouldFocusOnOpen = false;
let channelMenuPopover = null;
let channelMenuOpen = false;
let currentVideoLoadToken = 0;
let currentProfileLoadToken = 0;
@@ -266,127 +265,218 @@ function getChannelShareButton() {
}
function getChannelMenuElement() {
if (cachedChannelMenu && !document.body.contains(cachedChannelMenu)) {
cachedChannelMenu = null;
if (cachedChannelMenu && cachedChannelMenu.isConnected) {
return cachedChannelMenu;
}
if (!cachedChannelMenu) {
cachedChannelMenu = document.getElementById("moreDropdown-channel-profile");
const doc = typeof document !== "undefined" ? document : null;
const existing = doc?.getElementById("channelProfileMoreMenu") || null;
const HTMLElementCtor =
doc?.defaultView?.HTMLElement ||
(typeof HTMLElement !== "undefined" ? HTMLElement : null);
if (HTMLElementCtor && existing instanceof HTMLElementCtor) {
cachedChannelMenu = existing;
} else {
cachedChannelMenu = null;
}
return cachedChannelMenu;
}
function getZapControlsContainer() {
if (cachedZapControls && !document.body.contains(cachedZapControls)) {
if (cachedZapControls && cachedZapControls.isConnected) {
return cachedZapControls;
}
const doc = typeof document !== "undefined" ? document : null;
const existing = doc?.getElementById("zapControls") || null;
const HTMLElementCtor =
doc?.defaultView?.HTMLElement ||
(typeof HTMLElement !== "undefined" ? HTMLElement : null);
if (HTMLElementCtor && existing instanceof HTMLElementCtor) {
cachedZapControls = existing;
} else {
cachedZapControls = null;
}
if (!cachedZapControls) {
cachedZapControls = document.getElementById("zapControls");
}
if (cachedZapControls) {
if (!cachedZapControls.dataset.state) {
const isHidden =
cachedZapControls.hasAttribute("hidden") ||
cachedZapControls.getAttribute("aria-hidden") === "true";
cachedZapControls.dataset.state = isHidden ? "closed" : "open";
}
if (cachedZapControls.dataset.state !== "open") {
cachedZapControls.hidden = true;
cachedZapControls.setAttribute("aria-hidden", "true");
}
}
return cachedZapControls;
}
function getZapFormElement() {
if (cachedZapForm && !document.body.contains(cachedZapForm)) {
cachedZapForm = null;
}
if (!cachedZapForm) {
cachedZapForm = document.getElementById("zapForm");
if (cachedZapForm && cachedZapForm.isConnected) {
return cachedZapForm;
}
const container = getZapControlsContainer();
cachedZapForm = container?.querySelector("#zapForm") || null;
return cachedZapForm;
}
function getZapAmountInput() {
if (cachedZapAmountInput && !document.body.contains(cachedZapAmountInput)) {
cachedZapAmountInput = null;
}
if (!cachedZapAmountInput) {
cachedZapAmountInput = document.getElementById("zapAmountInput");
if (cachedZapAmountInput && cachedZapAmountInput.isConnected) {
return cachedZapAmountInput;
}
const container = getZapControlsContainer();
cachedZapAmountInput = container?.querySelector("#zapAmountInput") || null;
return cachedZapAmountInput;
}
function getZapSplitSummaryElement() {
if (cachedZapSplitSummary && !document.body.contains(cachedZapSplitSummary)) {
cachedZapSplitSummary = null;
}
if (!cachedZapSplitSummary) {
cachedZapSplitSummary = document.getElementById("zapSplitSummary");
if (cachedZapSplitSummary && cachedZapSplitSummary.isConnected) {
return cachedZapSplitSummary;
}
const container = getZapControlsContainer();
cachedZapSplitSummary = container?.querySelector("#zapSplitSummary") || null;
return cachedZapSplitSummary;
}
function getZapStatusElement() {
if (cachedZapStatus && !document.body.contains(cachedZapStatus)) {
cachedZapStatus = null;
}
if (!cachedZapStatus) {
cachedZapStatus = document.getElementById("zapStatus");
if (cachedZapStatus && cachedZapStatus.isConnected) {
return cachedZapStatus;
}
const container = getZapControlsContainer();
cachedZapStatus = container?.querySelector("#zapStatus") || null;
return cachedZapStatus;
}
function getZapReceiptsList() {
if (cachedZapReceipts && !document.body.contains(cachedZapReceipts)) {
cachedZapReceipts = null;
}
if (!cachedZapReceipts) {
cachedZapReceipts = document.getElementById("zapReceipts");
if (cachedZapReceipts && cachedZapReceipts.isConnected) {
return cachedZapReceipts;
}
const container = getZapControlsContainer();
cachedZapReceipts = container?.querySelector("#zapReceipts") || null;
return cachedZapReceipts;
}
function getZapSendButton() {
if (cachedZapSendBtn && !document.body.contains(cachedZapSendBtn)) {
cachedZapSendBtn = null;
}
if (!cachedZapSendBtn) {
cachedZapSendBtn = document.getElementById("zapSendBtn");
if (cachedZapSendBtn && cachedZapSendBtn.isConnected) {
return cachedZapSendBtn;
}
const container = getZapControlsContainer();
cachedZapSendBtn = container?.querySelector("#zapSendBtn") || null;
return cachedZapSendBtn;
}
function getZapWalletPrompt() {
if (cachedZapWalletPrompt && !document.body.contains(cachedZapWalletPrompt)) {
cachedZapWalletPrompt = null;
}
if (!cachedZapWalletPrompt) {
cachedZapWalletPrompt = document.getElementById("zapWalletPrompt");
if (cachedZapWalletPrompt && cachedZapWalletPrompt.isConnected) {
return cachedZapWalletPrompt;
}
const container = getZapControlsContainer();
cachedZapWalletPrompt = container?.querySelector("#zapWalletPrompt") || null;
return cachedZapWalletPrompt;
}
function getZapWalletLink() {
if (cachedZapWalletLink && !document.body.contains(cachedZapWalletLink)) {
cachedZapWalletLink = null;
}
if (!cachedZapWalletLink) {
cachedZapWalletLink = document.getElementById("zapWalletLink");
if (cachedZapWalletLink && cachedZapWalletLink.isConnected) {
return cachedZapWalletLink;
}
const container = getZapControlsContainer();
cachedZapWalletLink = container?.querySelector("#zapWalletLink") || null;
return cachedZapWalletLink;
}
function getZapCloseButton() {
if (cachedZapCloseBtn && !document.body.contains(cachedZapCloseBtn)) {
cachedZapCloseBtn = null;
}
if (!cachedZapCloseBtn) {
cachedZapCloseBtn = document.getElementById("zapCloseBtn");
if (cachedZapCloseBtn && cachedZapCloseBtn.isConnected) {
return cachedZapCloseBtn;
}
const container = getZapControlsContainer();
cachedZapCloseBtn = container?.querySelector("#zapCloseBtn") || null;
return cachedZapCloseBtn;
}
function cacheZapPanelElements(panel) {
if (!panel || panel.nodeType !== 1) {
cachedZapControls = null;
cachedZapForm = null;
cachedZapAmountInput = null;
cachedZapSplitSummary = null;
cachedZapStatus = null;
cachedZapReceipts = null;
cachedZapWalletPrompt = null;
cachedZapWalletLink = null;
cachedZapCloseBtn = null;
cachedZapSendBtn = null;
return;
}
cachedZapControls = panel;
cachedZapForm = panel.querySelector("#zapForm");
cachedZapAmountInput = panel.querySelector("#zapAmountInput");
cachedZapSplitSummary = panel.querySelector("#zapSplitSummary");
cachedZapStatus = panel.querySelector("#zapStatus");
cachedZapReceipts = panel.querySelector("#zapReceipts");
cachedZapWalletPrompt = panel.querySelector("#zapWalletPrompt");
cachedZapWalletLink = panel.querySelector("#zapWalletLink");
cachedZapCloseBtn = panel.querySelector("#zapCloseBtn");
cachedZapSendBtn = panel.querySelector("#zapSendBtn");
}
function initializeZapPanel() {
const zapButton = getChannelZapButton();
const amountInput = getZapAmountInput();
const zapForm = getZapFormElement();
const sendButton = getZapSendButton();
if (!zapButton || !amountInput || !zapForm || !sendButton) {
return;
}
zapButton.setAttribute("aria-expanded", "false");
if (zapForm.dataset.initialized !== "true") {
zapForm.addEventListener("submit", handleZapSend);
zapForm.dataset.initialized = "true";
}
if (amountInput.dataset.initialized !== "true") {
amountInput.addEventListener("input", handleZapAmountChange);
amountInput.addEventListener("change", handleZapAmountChange);
amountInput.dataset.initialized = "true";
}
const closeBtn = getZapCloseButton();
if (closeBtn && closeBtn.dataset.initialized !== "true") {
closeBtn.addEventListener("click", (event) => {
event?.preventDefault?.();
closeZapControls({ focusButton: true });
});
closeBtn.dataset.initialized = "true";
}
setupZapWalletLink();
const app = getApp();
const activeSettings =
typeof app?.getActiveNwcSettings === "function"
? app.getActiveNwcSettings()
: {};
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;
})();
const shouldShowPrompt = !zapButton.hasAttribute("hidden") && !hasWallet;
setZapWalletPromptVisible(shouldShowPrompt);
if (
Number.isFinite(activeSettings?.defaultZap) &&
activeSettings.defaultZap > 0
) {
amountInput.value = Math.max(0, Math.round(activeSettings.defaultZap));
} else if (!amountInput.value) {
amountInput.value = "";
}
updateZapSplitSummary();
setZapStatus("", "neutral");
clearZapReceipts();
}
function setZapWalletPromptVisible(visible) {
const prompt = getZapWalletPrompt();
if (!prompt) {
@@ -427,63 +517,57 @@ function focusZapAmountField() {
}
function isZapControlsOpen() {
if (typeof zapPopover?.isOpen === "function") {
return zapPopover.isOpen();
}
return zapControlsOpen;
}
function openZapControls({ focus = false } = {}) {
const controls = getZapControlsContainer();
const zapButton = getChannelZapButton();
if (!controls || !zapButton) {
if (!zapButton || !zapPopover) {
return false;
}
if (!zapControlsPositioner) {
const styles = createFloatingPanelStyles(controls);
const metricsDocument =
controls?.ownerDocument ||
zapButton?.ownerDocument ||
(typeof document !== "undefined" ? document : null);
const offset = getPopupOffsetPx({ documentRef: metricsDocument });
const viewportPadding = getPopupViewportPaddingPx({
documentRef: metricsDocument,
});
zapControlsPositioner = positionFloatingPanel(zapButton, controls, {
placement: "bottom",
alignment: "end",
offset,
viewportPadding,
styles,
zapShouldFocusOnOpen = focus;
const result = zapPopover.open();
if (result && typeof result.then === "function") {
result.catch(() => {}).finally(() => {
zapShouldFocusOnOpen = false;
});
return true;
}
if (!zapControlsOpen) {
controls.hidden = false;
controls.dataset.state = "open";
controls.setAttribute("aria-hidden", "false");
zapButton.setAttribute("aria-expanded", "true");
zapControlsOpen = true;
zapControlsPositioner?.update();
}
if (focus) {
focusZapAmountField();
}
return true;
zapShouldFocusOnOpen = false;
return Boolean(result);
}
function closeZapControls({ focusButton = false } = {}) {
const controls = getZapControlsContainer();
const zapButton = getChannelZapButton();
if (controls) {
controls.dataset.state = "closed";
controls.setAttribute("aria-hidden", "true");
controls.hidden = true;
zapShouldFocusOnOpen = false;
if (zapPopover) {
const result = zapPopover.close({ restoreFocus: focusButton });
if (!result && focusButton && zapButton && typeof zapButton.focus === "function") {
try {
zapButton.focus({ preventScroll: true });
} catch (error) {
zapButton.focus();
}
}
return Boolean(result);
}
zapControlsOpen = false;
if (zapButton) {
zapButton.setAttribute("aria-expanded", "false");
if (focusButton && typeof zapButton.focus === "function") {
zapButton.focus();
try {
zapButton.focus({ preventScroll: true });
} catch (error) {
zapButton.focus();
}
}
}
zapControlsOpen = false;
return Boolean(controls && zapButton);
return false;
}
function updateZapSplitSummary({ overrideFee = null } = {}) {
@@ -578,9 +662,11 @@ function renderZapReceipts(receipts, { partial = false } = {}) {
list.innerHTML = "";
const doc = list.ownerDocument || (typeof document !== "undefined" ? document : null);
if (!Array.isArray(receipts) || receipts.length === 0) {
if (partial) {
const emptyItem = document.createElement("li");
const emptyItem = doc?.createElement?.("li");
emptyItem.className =
"rounded border border-border bg-panel/70 p-3 text-text";
emptyItem.textContent = "No receipts were returned for this attempt.";
@@ -594,21 +680,30 @@ function renderZapReceipts(receipts, { partial = false } = {}) {
return;
}
const li = document.createElement("li");
const li = doc?.createElement?.("li");
if (!li) {
return;
}
li.className = "rounded border border-border bg-panel/70 p-3";
const header = document.createElement("div");
const header = doc?.createElement?.("div");
if (!header) {
return;
}
header.className =
"flex items-center justify-between text-xs font-semibold uppercase tracking-wide text-text";
const shareType = receipt.recipientType || receipt.type || "creator";
const shareLabel = document.createElement("span");
const shareLabel = doc?.createElement?.("span");
shareLabel.textContent = `${describeShareType(shareType)} • ${Math.max(
0,
Math.round(Number(receipt.amount || 0))
)} sats`;
const status = document.createElement("span");
const status = doc?.createElement?.("span");
if (!shareLabel || !status) {
return;
}
const isSuccess = receipt.status
? receipt.status === "success"
: !receipt.error;
@@ -619,15 +714,20 @@ function renderZapReceipts(receipts, { partial = false } = {}) {
header.appendChild(status);
li.appendChild(header);
const address = document.createElement("p");
address.className = "mt-1 text-xs text-text break-all";
const address = doc?.createElement?.("p");
if (address) {
address.className = "mt-1 text-xs text-text break-all";
}
const addressValue = receipt.address || "";
if (addressValue) {
if (address && addressValue) {
address.textContent = addressValue;
li.appendChild(address);
}
const detail = document.createElement("p");
const detail = doc?.createElement?.("p");
if (!detail) {
return;
}
detail.className = "mt-2 text-xs text-muted";
if (isSuccess) {
let detailMessage = "Invoice settled.";
@@ -1343,12 +1443,185 @@ function setupChannelShareButton() {
}
function setupChannelMoreMenu() {
const container = document.querySelector(".channel-profile-container");
if (!container) {
const doc = typeof document !== "undefined" ? document : null;
const moreBtn = doc?.getElementById("channelMoreBtn") || null;
if (!moreBtn) {
if (channelMenuPopover?.destroy) {
channelMenuPopover.destroy();
}
channelMenuPopover = null;
cachedChannelMenu = null;
channelMenuOpen = false;
return;
}
if (channelMenuPopover?.destroy) {
channelMenuPopover.destroy();
channelMenuPopover = null;
}
const documentRef =
moreBtn.ownerDocument || (typeof document !== "undefined" ? document : null);
const render = ({ document: docRef }) => {
const panel = createChannelProfileMenuPanel({
document: docRef,
context: "channel-profile",
});
if (!panel) {
return null;
}
panel.id = "channelProfileMoreMenu";
panel.dataset.state = panel.dataset.state || "closed";
panel.setAttribute("aria-hidden", "true");
panel.hidden = true;
cachedChannelMenu = panel;
const buttons = panel.querySelectorAll("button[data-action]");
buttons.forEach((button) => {
if (button.dataset.channelMenuBound === "true") {
return;
}
button.dataset.channelMenuBound = "true";
button.addEventListener("click", async (event) => {
event.preventDefault();
event.stopPropagation();
const dataset = {};
Object.entries(button.dataset || {}).forEach(([key, value]) => {
if (key === "channelMenuBound") {
return;
}
dataset[key] = value;
});
if (!dataset.context) {
dataset.context = "channel-profile";
}
if (!dataset.author && currentChannelHex) {
dataset.author = currentChannelHex;
}
if (!dataset.npub && currentChannelNpub) {
dataset.npub = currentChannelNpub;
}
const action = dataset.action || "";
try {
await handleChannelMenuAction(action, dataset);
} finally {
channelMenuPopover?.close();
}
});
});
updateChannelMenuState();
return panel;
};
const popover = createPopover(moreBtn, render, {
document: documentRef,
placement: "bottom-end",
restoreFocusOnClose: true,
});
if (!popover) {
return;
}
const originalOpen = popover.open?.bind(popover);
if (originalOpen) {
popover.open = async (...args) => {
const result = await originalOpen(...args);
if (result) {
channelMenuOpen = true;
const panel = getChannelMenuElement();
if (panel) {
panel.dataset.state = "open";
panel.hidden = false;
panel.setAttribute("aria-hidden", "false");
}
moreBtn.setAttribute("aria-expanded", "true");
}
return result;
};
}
const originalClose = popover.close?.bind(popover);
if (originalClose) {
popover.close = (options = {}) => {
const wasOpen = typeof popover.isOpen === "function" && popover.isOpen();
const result = originalClose(options);
if (wasOpen && result) {
channelMenuOpen = false;
const panel = getChannelMenuElement();
if (panel) {
panel.dataset.state = "closed";
panel.hidden = true;
panel.setAttribute("aria-hidden", "true");
}
moreBtn.setAttribute("aria-expanded", "false");
}
return result;
};
}
const originalDestroy = popover.destroy?.bind(popover);
if (originalDestroy) {
popover.destroy = (...args) => {
originalDestroy(...args);
if (channelMenuPopover === popover) {
channelMenuPopover = null;
}
channelMenuOpen = false;
cachedChannelMenu = null;
};
}
channelMenuPopover = popover;
if (moreBtn.dataset.initialized !== "true") {
moreBtn.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
if (!channelMenuPopover) {
return;
}
channelMenuPopover.toggle();
});
moreBtn.dataset.initialized = "true";
}
moreBtn.setAttribute("aria-expanded", "false");
}
async function handleChannelMenuAction(action, dataset = {}) {
const normalized = typeof action === "string" ? action.trim() : "";
if (!normalized) {
return;
}
const detail = { ...dataset };
if (!detail.context) {
detail.context = "channel-profile";
}
if (!detail.author && currentChannelHex) {
detail.author = currentChannelHex;
}
if (!detail.npub && currentChannelNpub) {
detail.npub = currentChannelNpub;
}
const app = getApp();
app?.attachMoreMenuHandlers?.(container);
try {
await app?.handleMoreMenuAction?.(normalized, detail);
} catch (error) {
userLogger.error("Failed to handle channel menu action:", error);
}
}
async function updateChannelMenuState() {
@@ -1372,7 +1645,7 @@ async function updateChannelMenuState() {
const buttons = menu.querySelectorAll("button[data-action]");
buttons.forEach((button) => {
if (!(button instanceof HTMLElement)) {
if (!button || button.nodeType !== 1) {
return;
}
@@ -1519,60 +1792,194 @@ export async function initChannelProfileView() {
function setupZapButton() {
const zapButton = getChannelZapButton();
const amountInput = getZapAmountInput();
const controls = getZapControlsContainer();
const zapForm = getZapFormElement();
if (
!zapButton ||
!amountInput ||
!controls ||
!zapForm ||
!getZapSendButton()
) {
if (!zapButton) {
return;
}
if (zapPopover?.destroy) {
zapPopover.destroy();
zapPopover = null;
}
const documentRef =
zapButton.ownerDocument ||
(typeof document !== "undefined" ? document : null);
const render = ({ document: doc }) => {
if (!doc || typeof doc.createElement !== "function") {
return null;
}
const panel = doc.createElement("div");
panel.id = "zapControls";
panel.className = "popover__panel card w-72 max-w-popover-safe p-4";
panel.dataset.variant = "zap";
panel.setAttribute("role", "dialog");
panel.setAttribute("aria-label", "Channel zap dialog");
panel.setAttribute("aria-hidden", "true");
panel.dataset.state = panel.dataset.state || "closed";
panel.hidden = true;
panel.innerHTML = `
<div class="grid gap-4">
<div class="flex items-center justify-between gap-4">
<h3 class="text-xs font-semibold uppercase tracking-wide text-muted-strong">
Send a zap
</h3>
<button
type="button"
id="zapCloseBtn"
class="btn-ghost px-2 py-1 text-xs"
aria-label="Close zap dialog"
>
✕
</button>
</div>
<p
id="zapWalletPrompt"
class="text-sm text-muted"
aria-hidden="true"
hidden
>
Connect a wallet in
<button
type="button"
id="zapWalletLink"
class="btn-ghost px-2 py-1 text-xs"
>
Wallet Connect settings
</button>
to send zaps.
</p>
<form id="zapForm" class="bv-stack bv-stack--tight">
<div>
<label
for="zapAmountInput"
id="zapAmountLabel"
class="mb-2 block text-xs font-semibold uppercase tracking-wide text-muted-strong"
>
Zap amount (sats)
</label>
<input
id="zapAmountInput"
type="number"
min="1"
step="1"
inputmode="numeric"
class="input"
placeholder="Enter sats"
aria-labelledby="zapAmountLabel"
/>
</div>
<p
id="zapSplitSummary"
class="text-sm text-muted"
aria-live="polite"
>
Enter an amount to view the split.
</p>
<p
id="zapStatus"
class="text-xs text-muted"
role="status"
aria-live="polite"
></p>
<ul
id="zapReceipts"
class="space-y-2 text-xs text-muted"
></ul>
<div class="flex items-center justify-end gap-2">
<button
type="submit"
id="zapSendBtn"
class="btn"
aria-label="Send a zap"
>
Send
</button>
</div>
</form>
</div>
`;
cacheZapPanelElements(panel);
initializeZapPanel();
return panel;
};
const popover = createPopover(zapButton, render, {
document: documentRef,
placement: "bottom-end",
restoreFocusOnClose: true,
maxWidthToken: "max-w-popover-safe",
});
if (!popover) {
return;
}
const originalOpen = popover.open?.bind(popover);
if (originalOpen) {
popover.open = async (...args) => {
const result = await originalOpen(...args);
if (result) {
zapControlsOpen = true;
const controls = getZapControlsContainer();
if (controls) {
controls.dataset.state = "open";
controls.hidden = false;
controls.setAttribute("aria-hidden", "false");
}
zapButton.setAttribute("aria-expanded", "true");
if (zapShouldFocusOnOpen) {
focusZapAmountField();
}
}
zapShouldFocusOnOpen = false;
return result;
};
}
const originalClose = popover.close?.bind(popover);
if (originalClose) {
popover.close = (options = {}) => {
const wasOpen = typeof popover.isOpen === "function" && popover.isOpen();
const result = originalClose(options);
if (wasOpen && result) {
zapControlsOpen = false;
const controls = getZapControlsContainer();
if (controls) {
controls.dataset.state = "closed";
controls.hidden = true;
controls.setAttribute("aria-hidden", "true");
}
zapButton.setAttribute("aria-expanded", "false");
}
zapShouldFocusOnOpen = false;
return result;
};
}
const originalDestroy = popover.destroy?.bind(popover);
if (originalDestroy) {
popover.destroy = (...args) => {
originalDestroy(...args);
if (zapPopover === popover) {
zapPopover = null;
}
cacheZapPanelElements(null);
};
}
zapPopover = popover;
if (zapButton.dataset.initialized !== "true") {
zapButton.addEventListener("click", handleZapButtonClick);
zapButton.dataset.initialized = "true";
}
setChannelZapVisibility(false);
controls.dataset.state = "closed";
controls.hidden = true;
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();
return;
}
const app = getApp();
const activeSettings =
typeof app?.getActiveNwcSettings === "function"
? app.getActiveNwcSettings()
: {};
if (
Number.isFinite(activeSettings?.defaultZap) &&
activeSettings.defaultZap > 0
) {
amountInput.value = Math.max(0, Math.round(activeSettings.defaultZap));
}
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";
}
/**
+48
View File
@@ -282,6 +282,54 @@ export function createVideoMoreMenuPanel({
return panel;
}
export function createChannelProfileMenuPanel({
document: documentRef = null,
context = "channel-profile",
designSystem = null,
} = {}) {
const doc = resolveDocument(documentRef);
if (!doc) {
return null;
}
normalizeDesignSystemContext(designSystem);
const normalizedContext =
typeof context === "string" && context ? context : "channel-profile";
const { panel, list } = ensureMenuContainer(doc, ["w-48", "p-0"]);
panel.dataset.menu = "channel-profile";
panel.dataset.menuContext = normalizedContext;
appendMenuAction(doc, list, {
text: "Copy npub",
action: "copy-npub",
dataset: { context: normalizedContext },
});
appendMenuAction(doc, list, {
text: "Blacklist channel",
action: "blacklist-author",
variant: "critical",
dataset: { context: normalizedContext },
});
appendMenuAction(doc, list, {
text: "Block channel",
action: "block-author",
variant: "critical",
dataset: { context: normalizedContext },
});
appendMenuAction(doc, list, {
text: "Report",
action: "report",
dataset: { context: normalizedContext },
});
return panel;
}
export function createVideoSettingsMenuPanel({
document: documentRef = null,
video = null,
+26 -173
View File
@@ -31,115 +31,21 @@
</div>
<div class="flex flex-col items-end gap-3 self-start sm:self-end">
<div class="flex items-center gap-2">
<div class="popover" 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="popover__panel card w-72 max-w-popover-safe p-4"
data-variant="zap"
role="dialog"
aria-hidden="true"
aria-label="Channel zap dialog"
data-state="closed"
hidden
>
<div class="grid gap-4">
<div class="flex items-center justify-between gap-4">
<h3
class="text-xs font-semibold uppercase tracking-wide text-muted-strong"
>
Send a zap
</h3>
<button
type="button"
id="zapCloseBtn"
class="btn-ghost px-2 py-1 text-xs"
aria-label="Close zap dialog"
>
✕
</button>
</div>
<p
id="zapWalletPrompt"
class="text-sm text-muted"
aria-hidden="true"
hidden
>
Connect a wallet in
<button
type="button"
id="zapWalletLink"
class="btn-ghost px-2 py-1 text-xs"
>
Wallet Connect settings
</button>
to send zaps.
</p>
<form id="zapForm" class="bv-stack bv-stack--tight">
<div>
<label
for="zapAmountInput"
id="zapAmountLabel"
class="mb-2 block text-xs font-semibold uppercase tracking-wide text-muted-strong"
>
Zap amount (sats)
</label>
<input
id="zapAmountInput"
type="number"
min="1"
step="1"
inputmode="numeric"
class="input"
placeholder="Enter sats"
aria-labelledby="zapAmountLabel"
/>
</div>
<p
id="zapSplitSummary"
class="text-sm text-muted"
aria-live="polite"
>
Enter an amount to view the split.
</p>
<p
id="zapStatus"
class="text-xs text-muted"
role="status"
aria-live="polite"
></p>
<ul
id="zapReceipts"
class="space-y-2 text-xs text-muted"
></ul>
<div class="flex items-center justify-end gap-2">
<button
type="submit"
id="zapSendBtn"
class="btn"
aria-label="Send a zap"
>
Send
</button>
</div>
</form>
</div>
</div>
</div>
<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>
<button
id="channelShareBtn"
type="button"
@@ -152,70 +58,17 @@
class="icon-image"
/>
</button>
<div class="popover" data-more-menu-wrapper="true">
<button
id="channelMoreBtn"
type="button"
class="icon-button"
aria-haspopup="true"
aria-expanded="false"
aria-label="Channel options"
data-more-dropdown="channel-profile"
>
<img src="assets/svg/ellipsis.svg" alt="More" class="h-5 w-5" />
</button>
<div
id="moreDropdown-channel-profile"
class="popover__panel w-48"
role="menu"
data-more-menu="true"
data-state="closed"
aria-hidden="true"
hidden
>
<div class="menu" role="none">
<button
type="button"
class="menu__item justify-start"
role="menuitem"
data-action="copy-npub"
data-context="channel-profile"
>
Copy npub
</button>
<button
type="button"
class="menu__item justify-start"
role="menuitem"
data-action="blacklist-author"
data-context="channel-profile"
data-blacklist-menu="true"
data-variant="critical"
>
Blacklist channel
</button>
<button
type="button"
class="menu__item justify-start"
role="menuitem"
data-action="block-author"
data-context="channel-profile"
data-variant="critical"
>
Block channel
</button>
<button
type="button"
class="menu__item justify-start"
role="menuitem"
data-action="report"
data-context="channel-profile"
>
Report
</button>
</div>
</div>
</div>
<button
id="channelMoreBtn"
type="button"
class="icon-button"
aria-haspopup="true"
aria-expanded="false"
aria-label="Channel options"
aria-controls="channelProfileMoreMenu"
>
<img src="assets/svg/ellipsis.svg" alt="More" class="h-5 w-5" />
</button>
</div>
</div>
</div>