Refactor DM Zap button to Composer area

Moves the Zap button from the message thread header to the message composer area, adjacent to the Send button. Refactors the Zap interface creation logic into a standalone component `ZapInterface.js` to decouple it from `MessageThread.js`. Updates `Composer.js` to support an optional `onZap` handler and renders the button with an icon-only style matching the video player's accent interaction. Updates `AppShell.js` to orchestrate the Zap interface display.
This commit is contained in:
google-labs-jules[bot] committed 2026-01-21 20:35:14 +00:00
1 parent 34d57c2072
commit d16808bcdd
6 files changed
+208 -160

No files matched your search

-123
View File
@@ -1,9 +1,6 @@
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);
@@ -101,21 +98,6 @@ 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");
@@ -188,108 +170,3 @@ export function MessageThread({
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 recent zaps.",
}),
);
return container;
}