mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Refactor DM UI: Move privacy settings, unify Zaps interface, and adjust layout
This commit is contained in:
1 parent
f4cff7313a
commit
07abc8516f
6 files changed
+266
-212
No files matched your search
+51
-9
@@ -2,6 +2,7 @@ import { Composer } from "./Composer.js";
|
||||
import { ConversationList } from "./ConversationList.js";
|
||||
import { MessageThread } from "./MessageThread.js";
|
||||
import { NotificationCenter } from "./NotificationCenter.js";
|
||||
import { DMPrivacySettings } from "./DMPrivacySettings.js";
|
||||
import { aggregateZapTotals } from "./zapHelpers.js";
|
||||
|
||||
function createElement(doc, tag, className, text) {
|
||||
@@ -121,6 +122,7 @@ export class AppShell {
|
||||
threadErrorType = "",
|
||||
composerState = "idle",
|
||||
privacyMode = "nip04",
|
||||
dmPrivacySettings = { readReceiptsEnabled: false, typingIndicatorsEnabled: false },
|
||||
notifications = [],
|
||||
signingAdapter = null,
|
||||
zapConfig = null,
|
||||
@@ -129,6 +131,8 @@ export class AppShell {
|
||||
onSendZap,
|
||||
onMarkConversationRead,
|
||||
onMarkAllRead,
|
||||
onToggleReadReceipts,
|
||||
onToggleTypingIndicators,
|
||||
} = {}) {
|
||||
if (!doc) {
|
||||
throw new Error("AppShell requires a document reference.");
|
||||
@@ -153,13 +157,47 @@ export class AppShell {
|
||||
);
|
||||
|
||||
const sidebar = createElement(doc, "aside", "dm-app-shell__sidebar");
|
||||
const sidebarHeader = createElement(doc, "div", "dm-app-shell__sidebar-header flex items-center justify-between p-4");
|
||||
const sidebarTitle = createElement(
|
||||
doc,
|
||||
"h1",
|
||||
"dm-app-shell__title",
|
||||
"dm-app-shell__title text-lg font-bold",
|
||||
"Direct Messages",
|
||||
);
|
||||
sidebar.appendChild(sidebarTitle);
|
||||
sidebarHeader.appendChild(sidebarTitle);
|
||||
|
||||
const settingsBtn = createElement(doc, "button", "btn-ghost btn-icon");
|
||||
settingsBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>`;
|
||||
settingsBtn.ariaLabel = "Direct message settings";
|
||||
settingsBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
let popover = sidebar.querySelector(".dm-settings-popover");
|
||||
if (popover) {
|
||||
popover.remove();
|
||||
return;
|
||||
}
|
||||
popover = DMPrivacySettings({
|
||||
document: doc,
|
||||
readReceiptsEnabled: dmPrivacySettings.readReceiptsEnabled,
|
||||
typingIndicatorsEnabled: dmPrivacySettings.typingIndicatorsEnabled,
|
||||
onToggleReadReceipts,
|
||||
onToggleTypingIndicators,
|
||||
});
|
||||
popover.classList.add("dm-settings-popover", "absolute", "z-50", "right-4", "top-12", "bg-surface", "border", "border-border", "rounded-xl");
|
||||
sidebarHeader.style.position = "relative";
|
||||
sidebarHeader.appendChild(popover);
|
||||
|
||||
const closeHandler = (event) => {
|
||||
if (!popover.contains(event.target) && event.target !== settingsBtn && !settingsBtn.contains(event.target)) {
|
||||
popover.remove();
|
||||
doc.removeEventListener("click", closeHandler);
|
||||
}
|
||||
};
|
||||
doc.addEventListener("click", closeHandler);
|
||||
});
|
||||
sidebarHeader.appendChild(settingsBtn);
|
||||
sidebar.appendChild(sidebarHeader);
|
||||
|
||||
sidebar.appendChild(
|
||||
ConversationList({
|
||||
document: doc,
|
||||
@@ -172,12 +210,7 @@ export class AppShell {
|
||||
);
|
||||
|
||||
const main = createElement(doc, "main", "dm-app-shell__main");
|
||||
main.appendChild(
|
||||
NotificationCenter({
|
||||
document: doc,
|
||||
notices: notifications,
|
||||
}),
|
||||
);
|
||||
|
||||
main.appendChild(
|
||||
MessageThread({
|
||||
document: doc,
|
||||
@@ -191,6 +224,8 @@ export class AppShell {
|
||||
totalsByConversation.get(activeConversation.id) || 0,
|
||||
profileZapTotalSats: totalsByProfile.get(activeConversation.pubkey) || 0,
|
||||
onMarkRead: onMarkConversationRead,
|
||||
onSendZap,
|
||||
zapConfig,
|
||||
}),
|
||||
);
|
||||
main.appendChild(
|
||||
@@ -202,7 +237,14 @@ export class AppShell {
|
||||
zapRecipient: activeConversation,
|
||||
zapConfig,
|
||||
onSend: onSendMessage,
|
||||
onSendZap,
|
||||
// Zap button moved to MessageThread, so onSendZap here might be redundant if triggered from thread
|
||||
// But Composer still handles text message sending.
|
||||
}),
|
||||
);
|
||||
main.appendChild(
|
||||
NotificationCenter({
|
||||
document: doc,
|
||||
notices: notifications,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { createAndPublishZapRequest, resolveZapRecipient } from "../../payments/zapRequests.js";
|
||||
import { devLogger, userLogger } from "../../utils/logger.js";
|
||||
|
||||
function createElement(doc, tag, className, text) {
|
||||
const element = doc.createElement(tag);
|
||||
if (className) {
|
||||
@@ -21,7 +18,6 @@ export function Composer({
|
||||
zapRecipient = null,
|
||||
zapConfig = null,
|
||||
onSend,
|
||||
onSendZap,
|
||||
} = {}) {
|
||||
if (!doc) {
|
||||
throw new Error("Composer requires a document reference.");
|
||||
@@ -87,15 +83,7 @@ export function Composer({
|
||||
privacyToggle.setAttribute("aria-pressed", "false");
|
||||
privacyToggle.setAttribute("aria-label", "Toggle privacy mode");
|
||||
|
||||
const zapToggle = createElement(doc, "button", "dm-composer__menu-item", "Zap");
|
||||
zapToggle.type = "button";
|
||||
zapToggle.setAttribute("role", "menuitem");
|
||||
zapToggle.setAttribute("aria-expanded", "false");
|
||||
zapToggle.setAttribute("aria-label", "Open zap composer");
|
||||
zapToggle.setAttribute("aria-pressed", "false");
|
||||
|
||||
moreMenu.appendChild(privacyToggle);
|
||||
moreMenu.appendChild(zapToggle);
|
||||
|
||||
tools.appendChild(attachButton);
|
||||
tools.appendChild(moreButton);
|
||||
@@ -126,64 +114,8 @@ export function Composer({
|
||||
status.textContent = "Sending…";
|
||||
}
|
||||
|
||||
const zapPanel = createElement(doc, "div", "dm-composer__zap-panel");
|
||||
zapPanel.id = "dm-composer-zap-panel";
|
||||
zapPanel.hidden = true;
|
||||
zapToggle.setAttribute("aria-controls", zapPanel.id);
|
||||
const zapRecipientLabel = createElement(
|
||||
doc,
|
||||
"span",
|
||||
"dm-composer__zap-recipient",
|
||||
zapRecipient?.name
|
||||
? `Zap ${zapRecipient.name}`
|
||||
: "Zap this contact",
|
||||
);
|
||||
zapPanel.appendChild(zapRecipientLabel);
|
||||
|
||||
const zapAddress = createElement(
|
||||
doc,
|
||||
"span",
|
||||
"dm-composer__zap-address",
|
||||
zapRecipient?.lightningAddress || "Lightning address required.",
|
||||
);
|
||||
zapPanel.appendChild(zapAddress);
|
||||
|
||||
const zapAmountLabel = createElement(doc, "label", "dm-composer__zap-label", "Amount (sats)");
|
||||
zapAmountLabel.setAttribute("for", "dm-zap-amount-input");
|
||||
const zapAmountInput = createElement(doc, "input", "dm-composer__zap-input");
|
||||
zapAmountInput.type = "number";
|
||||
zapAmountInput.min = "1";
|
||||
zapAmountInput.step = "1";
|
||||
zapAmountInput.inputMode = "numeric";
|
||||
zapAmountInput.id = "dm-zap-amount-input";
|
||||
zapAmountInput.placeholder = "100";
|
||||
zapAmountInput.setAttribute("aria-label", "Zap amount in sats");
|
||||
zapPanel.appendChild(zapAmountLabel);
|
||||
zapPanel.appendChild(zapAmountInput);
|
||||
|
||||
const zapNoteLabel = createElement(doc, "label", "dm-composer__zap-label", "Zap note");
|
||||
zapNoteLabel.setAttribute("for", "dm-zap-note-input");
|
||||
const zapNoteInput = createElement(doc, "input", "dm-composer__zap-input");
|
||||
zapNoteInput.type = "text";
|
||||
zapNoteInput.id = "dm-zap-note-input";
|
||||
zapNoteInput.placeholder = "Optional note for this zap";
|
||||
zapNoteInput.setAttribute("aria-label", "Zap note");
|
||||
zapPanel.appendChild(zapNoteLabel);
|
||||
zapPanel.appendChild(zapNoteInput);
|
||||
|
||||
const zapActions = createElement(doc, "div", "dm-composer__zap-actions");
|
||||
const zapSendButton = createElement(doc, "button", "dm-composer__zap-send", "Send zap");
|
||||
zapSendButton.type = "button";
|
||||
zapSendButton.setAttribute("aria-label", "Send zap request");
|
||||
zapActions.appendChild(zapSendButton);
|
||||
zapPanel.appendChild(zapActions);
|
||||
|
||||
const zapStatus = createElement(doc, "div", "dm-composer__zap-status");
|
||||
zapPanel.appendChild(zapStatus);
|
||||
|
||||
form.appendChild(label);
|
||||
form.appendChild(textarea);
|
||||
form.appendChild(zapPanel);
|
||||
form.appendChild(actions);
|
||||
form.appendChild(status);
|
||||
|
||||
@@ -213,48 +145,6 @@ export function Composer({
|
||||
onSend(textarea.value, payload);
|
||||
};
|
||||
|
||||
let zapOpen = false;
|
||||
let resolvedRecipient = null;
|
||||
|
||||
const setZapStatus = (message, variant = "info") => {
|
||||
zapStatus.textContent = message || "";
|
||||
zapStatus.dataset.state = variant;
|
||||
};
|
||||
|
||||
const resolveRecipient = async () => {
|
||||
if (!zapRecipient?.lightningAddress) {
|
||||
setZapStatus("Add a Lightning address to enable zaps.", "error");
|
||||
return null;
|
||||
}
|
||||
setZapStatus("Resolving LNURL…", "loading");
|
||||
try {
|
||||
const resolver =
|
||||
typeof zapConfig?.resolveRecipient === "function"
|
||||
? zapConfig.resolveRecipient
|
||||
: resolveZapRecipient;
|
||||
resolvedRecipient = await resolver(zapRecipient.lightningAddress, {
|
||||
fetcher: zapConfig?.fetcher,
|
||||
});
|
||||
if (
|
||||
!resolvedRecipient?.metadata?.allowsNostr &&
|
||||
!resolvedRecipient?.metadata?.nostrPubkey
|
||||
) {
|
||||
setZapStatus("Recipient LNURL does not support Nostr zaps.", "error");
|
||||
resolvedRecipient = null;
|
||||
return null;
|
||||
}
|
||||
setZapStatus("LNURL ready for zaps.", "success");
|
||||
return resolvedRecipient;
|
||||
} catch (error) {
|
||||
devLogger.warn("[dm] Failed to resolve LNURL for zap recipient.", error);
|
||||
setZapStatus(
|
||||
error instanceof Error ? error.message : "Failed to resolve LNURL.",
|
||||
"error",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
void handleSubmit();
|
||||
@@ -281,77 +171,6 @@ export function Composer({
|
||||
updatePrivacyLabel();
|
||||
});
|
||||
|
||||
zapToggle.addEventListener("click", () => {
|
||||
zapOpen = !zapOpen;
|
||||
zapPanel.hidden = !zapOpen;
|
||||
zapToggle.setAttribute("aria-expanded", zapOpen ? "true" : "false");
|
||||
zapToggle.setAttribute("aria-pressed", zapOpen ? "true" : "false");
|
||||
if (zapOpen) {
|
||||
void resolveRecipient();
|
||||
} else {
|
||||
setZapStatus("");
|
||||
}
|
||||
});
|
||||
|
||||
zapSendButton.addEventListener("click", async () => {
|
||||
const amount = Number(zapAmountInput.value);
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
setZapStatus("Enter a zap amount greater than zero.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const resolved = resolvedRecipient || (await resolveRecipient());
|
||||
if (!resolved) {
|
||||
return;
|
||||
}
|
||||
|
||||
const comment = zapNoteInput.value.trim();
|
||||
const payload = {
|
||||
amountSats: Math.round(amount),
|
||||
comment,
|
||||
recipient: zapRecipient,
|
||||
resolved,
|
||||
};
|
||||
|
||||
if (typeof onSendZap === "function") {
|
||||
try {
|
||||
await onSendZap(payload);
|
||||
setZapStatus("Zap request sent. Awaiting receipt…", "success");
|
||||
} catch (error) {
|
||||
userLogger.error("[dm] Zap request failed.", error);
|
||||
setZapStatus(
|
||||
error instanceof Error ? error.message : "Zap request failed.",
|
||||
"error",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (zapConfig?.signer) {
|
||||
try {
|
||||
await createAndPublishZapRequest({
|
||||
address: zapRecipient?.lightningAddress,
|
||||
recipientPubkey: zapRecipient?.pubkey,
|
||||
relays: zapRecipient?.relayHints || zapConfig?.relays,
|
||||
amountSats: Math.round(amount),
|
||||
comment,
|
||||
signer: zapConfig.signer,
|
||||
pool: zapConfig.pool,
|
||||
fetcher: zapConfig.fetcher,
|
||||
});
|
||||
setZapStatus("Zap request published. Awaiting receipt…", "success");
|
||||
} catch (error) {
|
||||
userLogger.error("[dm] Zap publish failed.", error);
|
||||
setZapStatus(
|
||||
error instanceof Error ? error.message : "Zap request failed.",
|
||||
"error",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setZapStatus("Connect a Nostr signer to send zap requests.", "error");
|
||||
}
|
||||
});
|
||||
|
||||
textarea.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
export function DMPrivacySettings({
|
||||
document: doc,
|
||||
readReceiptsEnabled = false,
|
||||
typingIndicatorsEnabled = false,
|
||||
onToggleReadReceipts,
|
||||
onToggleTypingIndicators,
|
||||
} = {}) {
|
||||
if (!doc) {
|
||||
throw new Error("DMPrivacySettings requires a document reference.");
|
||||
}
|
||||
|
||||
const root = doc.createElement("div");
|
||||
root.className = "dm-privacy-settings card p-4 space-y-4 shadow-lg";
|
||||
|
||||
const title = doc.createElement("h4");
|
||||
title.className = "text-sm font-semibold text-text";
|
||||
title.textContent = "Direct message privacy";
|
||||
root.appendChild(title);
|
||||
|
||||
const desc = doc.createElement("p");
|
||||
desc.className = "text-xs text-muted mb-4";
|
||||
desc.textContent = "Control read receipts and typing indicators for direct messages.";
|
||||
root.appendChild(desc);
|
||||
|
||||
// Read Receipts Toggle
|
||||
const readReceiptsRow = doc.createElement("div");
|
||||
readReceiptsRow.className = "flex items-center justify-between";
|
||||
|
||||
const readReceiptsLabel = doc.createElement("label");
|
||||
readReceiptsLabel.className = "text-sm text-text cursor-pointer";
|
||||
readReceiptsLabel.textContent = "Read receipts";
|
||||
readReceiptsLabel.setAttribute("for", "dm-read-receipts-toggle");
|
||||
readReceiptsRow.appendChild(readReceiptsLabel);
|
||||
|
||||
const readReceiptsToggle = doc.createElement("input");
|
||||
readReceiptsToggle.type = "checkbox";
|
||||
readReceiptsToggle.id = "dm-read-receipts-toggle";
|
||||
readReceiptsToggle.className = "toggle toggle-sm toggle-accent";
|
||||
readReceiptsToggle.checked = readReceiptsEnabled;
|
||||
readReceiptsToggle.addEventListener("change", (e) => {
|
||||
if (typeof onToggleReadReceipts === "function") {
|
||||
onToggleReadReceipts(e.target.checked);
|
||||
}
|
||||
});
|
||||
readReceiptsRow.appendChild(readReceiptsToggle);
|
||||
root.appendChild(readReceiptsRow);
|
||||
|
||||
// Typing Indicators Toggle
|
||||
const typingIndicatorsRow = doc.createElement("div");
|
||||
typingIndicatorsRow.className = "flex items-center justify-between";
|
||||
|
||||
const typingIndicatorsLabel = doc.createElement("label");
|
||||
typingIndicatorsLabel.className = "text-sm text-text cursor-pointer";
|
||||
typingIndicatorsLabel.textContent = "Typing indicators";
|
||||
typingIndicatorsLabel.setAttribute("for", "dm-typing-indicators-toggle");
|
||||
typingIndicatorsRow.appendChild(typingIndicatorsLabel);
|
||||
|
||||
const typingIndicatorsToggle = doc.createElement("input");
|
||||
typingIndicatorsToggle.type = "checkbox";
|
||||
typingIndicatorsToggle.id = "dm-typing-indicators-toggle";
|
||||
typingIndicatorsToggle.className = "toggle toggle-sm toggle-accent";
|
||||
typingIndicatorsToggle.checked = typingIndicatorsEnabled;
|
||||
typingIndicatorsToggle.addEventListener("change", (e) => {
|
||||
if (typeof onToggleTypingIndicators === "function") {
|
||||
onToggleTypingIndicators(e.target.checked);
|
||||
}
|
||||
});
|
||||
typingIndicatorsRow.appendChild(typingIndicatorsToggle);
|
||||
root.appendChild(typingIndicatorsRow);
|
||||
|
||||
return root;
|
||||
}
|
||||
@@ -33,7 +33,9 @@ export function MessageBubble({
|
||||
|
||||
const meta = createElement(doc, "div", "dm-message-bubble__meta");
|
||||
if (timestamp) {
|
||||
meta.appendChild(createElement(doc, "span", "dm-message-bubble__time", timestamp));
|
||||
// Added 'text-muted' class for theme-respecting color (usually handles light/dark text)
|
||||
const timeSpan = createElement(doc, "span", "dm-message-bubble__time text-muted", timestamp);
|
||||
meta.appendChild(timeSpan);
|
||||
}
|
||||
|
||||
if (variant === "outgoing") {
|
||||
|
||||
+127
-4
@@ -2,6 +2,8 @@ import { DayDivider } from "./DayDivider.js";
|
||||
import { MessageBubble } from "./MessageBubble.js";
|
||||
import { ZapReceiptList } from "./ZapReceiptList.js";
|
||||
import { formatZapAmount } from "./zapHelpers.js";
|
||||
import { createAndPublishZapRequest, resolveZapRecipient } from "../../payments/zapRequests.js";
|
||||
import { userLogger, devLogger } from "../../utils/logger.js";
|
||||
|
||||
function createElement(doc, tag, className, text) {
|
||||
const element = doc.createElement(tag);
|
||||
@@ -25,6 +27,8 @@ export function MessageThread({
|
||||
conversationZapTotalSats = 0,
|
||||
profileZapTotalSats = 0,
|
||||
onMarkRead,
|
||||
onSendZap,
|
||||
zapConfig = null,
|
||||
} = {}) {
|
||||
if (!doc) {
|
||||
throw new Error("MessageThread requires a document reference.");
|
||||
@@ -84,6 +88,21 @@ export function MessageThread({
|
||||
);
|
||||
header.appendChild(zapSummary);
|
||||
|
||||
// Zap Button
|
||||
const zapBtn = createElement(doc, "button", "btn-ghost btn-icon");
|
||||
zapBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>`;
|
||||
zapBtn.ariaLabel = "Zap";
|
||||
zapBtn.addEventListener("click", () => {
|
||||
const existingInterface = root.querySelector(".dm-zap-interface");
|
||||
if (existingInterface) {
|
||||
existingInterface.remove();
|
||||
return;
|
||||
}
|
||||
const zapInterface = createZapInterface(doc, { contact, zapReceipts, onSendZap, zapConfig });
|
||||
root.appendChild(zapInterface);
|
||||
});
|
||||
header.appendChild(zapBtn);
|
||||
|
||||
if (typeof onMarkRead === "function") {
|
||||
const actions = createElement(doc, "div", "ml-auto flex items-center gap-2");
|
||||
const markReadButton = createElement(doc, "button", "btn-ghost", "Mark read");
|
||||
@@ -146,14 +165,118 @@ export function MessageThread({
|
||||
});
|
||||
}
|
||||
|
||||
root.appendChild(timeline);
|
||||
// Scroll to bottom
|
||||
if (messages.length > 0) {
|
||||
setTimeout(() => {
|
||||
timeline.scrollTop = timeline.scrollHeight;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
root.appendChild(
|
||||
root.appendChild(timeline);
|
||||
return root;
|
||||
}
|
||||
|
||||
function createZapInterface(doc, { contact, zapReceipts, onSendZap, zapConfig }) {
|
||||
const container = createElement(doc, "div", "dm-zap-interface card p-4 absolute bottom-16 right-4 z-20 w-80 shadow-xl border border-border bg-surface");
|
||||
|
||||
const closeBtn = createElement(doc, "button", "absolute top-2 right-2 btn-ghost btn-xs", "✕");
|
||||
closeBtn.addEventListener("click", () => container.remove());
|
||||
container.appendChild(closeBtn);
|
||||
|
||||
const title = createElement(doc, "h4", "text-sm font-bold mb-2", "Send Zap");
|
||||
container.appendChild(title);
|
||||
|
||||
// Zap Form
|
||||
const form = createElement(doc, "form", "space-y-2 mb-4");
|
||||
const amountInput = createElement(doc, "input", "input input-sm w-full");
|
||||
amountInput.type = "number";
|
||||
amountInput.placeholder = "Amount (sats)";
|
||||
amountInput.min = "1";
|
||||
|
||||
const noteInput = createElement(doc, "input", "input input-sm w-full");
|
||||
noteInput.type = "text";
|
||||
noteInput.placeholder = "Comment (optional)";
|
||||
|
||||
const sendBtn = createElement(doc, "button", "btn btn-sm btn-primary w-full", "Zap ⚡");
|
||||
const statusDiv = createElement(doc, "div", "text-xs text-center mt-1");
|
||||
|
||||
sendBtn.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
const amount = parseInt(amountInput.value);
|
||||
const comment = noteInput.value;
|
||||
if (!amount || amount <= 0) {
|
||||
statusDiv.textContent = "Invalid amount";
|
||||
statusDiv.className = "text-error text-xs text-center mt-1";
|
||||
return;
|
||||
}
|
||||
|
||||
statusDiv.textContent = "Sending...";
|
||||
statusDiv.className = "text-muted text-xs text-center mt-1";
|
||||
sendBtn.disabled = true;
|
||||
|
||||
try {
|
||||
if (typeof onSendZap === "function") {
|
||||
// Use provided callback if available (e.g. from controller)
|
||||
if (!contact.lightningAddress) {
|
||||
throw new Error("No lightning address for this contact");
|
||||
}
|
||||
|
||||
const resolver = zapConfig?.resolveRecipient || resolveZapRecipient;
|
||||
const resolved = await resolver(contact.lightningAddress, { fetcher: zapConfig?.fetcher });
|
||||
if (!resolved) throw new Error("Could not resolve lightning address");
|
||||
|
||||
const payload = {
|
||||
amountSats: amount,
|
||||
comment,
|
||||
recipient: contact,
|
||||
resolved
|
||||
};
|
||||
await onSendZap(payload);
|
||||
statusDiv.textContent = "Zap sent!";
|
||||
statusDiv.className = "text-success text-xs text-center mt-1";
|
||||
amountInput.value = "";
|
||||
noteInput.value = "";
|
||||
} else if (zapConfig?.signer) {
|
||||
await createAndPublishZapRequest({
|
||||
address: contact.lightningAddress,
|
||||
recipientPubkey: contact.pubkey,
|
||||
relays: contact.relayHints || zapConfig?.relays,
|
||||
amountSats: amount,
|
||||
comment,
|
||||
signer: zapConfig.signer,
|
||||
pool: zapConfig.pool,
|
||||
fetcher: zapConfig.fetcher,
|
||||
});
|
||||
statusDiv.textContent = "Zap sent!";
|
||||
statusDiv.className = "text-success text-xs text-center mt-1";
|
||||
amountInput.value = "";
|
||||
noteInput.value = "";
|
||||
} else {
|
||||
throw new Error("No signer available");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
statusDiv.textContent = "Failed to send zap";
|
||||
statusDiv.className = "text-error text-xs text-center mt-1";
|
||||
} finally {
|
||||
sendBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
form.appendChild(amountInput);
|
||||
form.appendChild(noteInput);
|
||||
form.appendChild(sendBtn);
|
||||
form.appendChild(statusDiv);
|
||||
container.appendChild(form);
|
||||
|
||||
// Receipts List
|
||||
container.appendChild(
|
||||
ZapReceiptList({
|
||||
document: doc,
|
||||
receipts: zapReceipts,
|
||||
emptyLabel: "No zap receipts published yet.",
|
||||
emptyLabel: "No recent zaps.",
|
||||
}),
|
||||
);
|
||||
return root;
|
||||
|
||||
return container;
|
||||
}
|
||||
@@ -4421,6 +4421,8 @@ export class ProfileModalController {
|
||||
container.innerHTML = "";
|
||||
|
||||
try {
|
||||
const dmPrivacySettings = this.getDmPrivacySettingsSnapshot();
|
||||
|
||||
this.dmAppShell = new AppShell({
|
||||
document,
|
||||
conversations,
|
||||
@@ -4429,8 +4431,12 @@ export class ProfileModalController {
|
||||
messages: timeline,
|
||||
threadState,
|
||||
privacyMode,
|
||||
dmPrivacySettings,
|
||||
composerState: this.dmComposerState || "idle",
|
||||
notifications: [],
|
||||
zapConfig: {
|
||||
signer: this.services.nostrClient,
|
||||
},
|
||||
onSelectConversation: (conversation) => {
|
||||
void this.handleDmConversationSelect(conversation);
|
||||
},
|
||||
@@ -4443,6 +4449,12 @@ export class ProfileModalController {
|
||||
onMarkAllRead: () => {
|
||||
void this.handleDmMarkAllConversationsRead();
|
||||
},
|
||||
onToggleReadReceipts: (enabled) => {
|
||||
this.handleReadReceiptsToggle(enabled);
|
||||
},
|
||||
onToggleTypingIndicators: (enabled) => {
|
||||
this.handleTypingIndicatorsToggle(enabled);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.dmAppShell = null;
|
||||
@@ -5160,23 +5172,7 @@ export class ProfileModalController {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.profileMessagesReadReceiptsToggle instanceof HTMLElement) {
|
||||
this.profileMessagesReadReceiptsToggle.addEventListener("change", (event) => {
|
||||
const toggle = event.currentTarget;
|
||||
if (toggle instanceof HTMLInputElement) {
|
||||
this.handleReadReceiptsToggle(toggle.checked);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.profileMessagesTypingToggle instanceof HTMLElement) {
|
||||
this.profileMessagesTypingToggle.addEventListener("change", (event) => {
|
||||
const toggle = event.currentTarget;
|
||||
if (toggle instanceof HTMLInputElement) {
|
||||
this.handleTypingIndicatorsToggle(toggle.checked);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Legacy toggles removed - handled by DMPrivacySettings in AppShell
|
||||
|
||||
if (this.profileLinkPreviewAutoToggle instanceof HTMLElement) {
|
||||
this.profileLinkPreviewAutoToggle.addEventListener("change", (event) => {
|
||||
|
||||
Reference in new issue
Block a user