mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-03 11:22:25 +00:00
Refactor channel profile popovers
This commit is contained in:
1 parent
6f9f239144
commit
62a2694c23
3 files changed
+654
-346
No files matched your search
+580
-173
@@ -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";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user