Refactor DM UI: Move privacy settings, unify Zaps interface, and adjust layout

This commit is contained in:
google-labs-jules[bot] committed 2026-01-20 02:04:10 +00:00
1 parent f4cff7313a
commit 07abc8516f
6 files changed
+266 -212

No files matched your search

+127 -4
View File
@@ -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;
}