UNPKG

@getalby/lightning-messageboard

Version:

A web component for a lightning messageboard powered by NWC

336 lines (335 loc) 13.4 kB
import { nwc } from "@getalby/sdk"; import { launchPaymentModal } from "@getalby/bitcoin-connect"; import { template } from "./template"; import { escapeHTML, formatNumber, showToast } from "./helper"; /** * LightningMessageboard Web Component * * A customizable web component for displaying a Lightning Network-powered messageboard * where users can pay to post messages. */ export class LightningMessageboard extends HTMLElement { // Observed attributes static get observedAttributes() { return ["nwc-url", "theme"]; } constructor() { super(); this.messages = []; this.messageText = ""; this.senderName = ""; this.amount = ""; this.topAmount = 1000; this.shadow = this.attachShadow({ mode: "open" }); this.shadow.appendChild(template.content.cloneNode(true)); this.initializeElements(); this.setupEventListeners(); } // Component lifecycle methods connectedCallback() { // Initialize when component is connected to the DOM this.applyTheme(); } attributeChangedCallback(name, oldValue, newValue) { if (oldValue === newValue) return; switch (name) { case "nwc-url": this.initializeNWC(); break; case "theme": this.applyTheme(); break; } } // Initialize DOM element references initializeElements() { this.messageList = this.shadow.getElementById("message-list"); this.toggleButton = this.shadow.getElementById("toggle-button"); this.cardContent = this.shadow.getElementById("card-content"); this.messageInput = this.shadow.getElementById("message-input"); this.messageForm = this.shadow.getElementById("message-form"); this.dialog = this.shadow.getElementById("dialog"); this.senderNameInput = this.shadow.getElementById("sender-name"); this.amountInput = this.shadow.getElementById("amount"); this.messageTextArea = this.shadow.getElementById("message-text"); this.topAmountButton = this.shadow.getElementById("top-amount"); this.cancelButton = this.shadow.getElementById("cancel-button"); this.confirmButton = this.shadow.getElementById("confirm-button"); this.confirmSpinner = this.shadow.getElementById("confirm-spinner"); this.loadingSpinner = this.shadow.getElementById("loading-spinner"); this.centeredLoadingSpinner = this.shadow.getElementById("centered-loading-spinner"); } // Set up event listeners setupEventListeners() { var _a, _b, _c, _d, _e, _f, _g, _h; // Message form (_a = this.messageForm) === null || _a === void 0 ? void 0 : _a.addEventListener("submit", (e) => this.handleSubmitOpenDialog(e)); // Payment form (_b = this.shadow .getElementById("payment-form")) === null || _b === void 0 ? void 0 : _b.addEventListener("submit", (e) => this.handleSubmitPayment(e)); // Dialog cancel (_c = this.cancelButton) === null || _c === void 0 ? void 0 : _c.addEventListener("click", () => this.setDialogOpen(false)); // Top amount button (_d = this.topAmountButton) === null || _d === void 0 ? void 0 : _d.addEventListener("click", () => { if (this.amountInput) { this.amountInput.value = this.topAmount.toString(); this.amount = this.topAmount.toString(); } }); // Form input changes (_e = this.messageInput) === null || _e === void 0 ? void 0 : _e.addEventListener("input", (e) => { this.messageText = e.target.value; }); (_f = this.senderNameInput) === null || _f === void 0 ? void 0 : _f.addEventListener("input", (e) => { this.senderName = e.target.value; }); (_g = this.amountInput) === null || _g === void 0 ? void 0 : _g.addEventListener("input", (e) => { this.amount = e.target.value; }); (_h = this.messageTextArea) === null || _h === void 0 ? void 0 : _h.addEventListener("input", (e) => { this.messageText = e.target.value; if (this.messageInput) { this.messageInput.value = this.messageText; } }); } // Initialize NWC client with the provided URL initializeNWC() { const nwcUrl = this.getAttribute("nwc-url"); if (!nwcUrl) { return; } this.nwcClient = new nwc.NWCClient({ nostrWalletConnectUrl: nwcUrl, }); // Load messages if the component is open this.loadMessages(); } // Apply theme colors applyTheme() { const theme = this.getAttribute("theme"); if (theme) { try { const themeObj = JSON.parse(theme); for (const [key, value] of Object.entries(themeObj)) { this.style.setProperty(`--lmb-${key}`, value); } } catch (e) { console.error("Invalid theme format", e); } } } // Show/hide the payment dialog setDialogOpen(open) { if (this.dialog) { this.dialog.classList.toggle("hidden", !open); } if (open) { // When opening the dialog, copy the message from input if (this.messageTextArea && this.messageInput) { this.messageTextArea.value = this.messageInput.value; this.messageText = this.messageInput.value; } // Focus the name input setTimeout(() => { var _a; (_a = this.senderNameInput) === null || _a === void 0 ? void 0 : _a.focus(); }, 100); } } // Handle the message form submission (opens the dialog) handleSubmitOpenDialog(e) { e.preventDefault(); this.setDialogOpen(true); } // Handle the payment form submission async handleSubmitPayment(e) { e.preventDefault(); // Validate amount if (Number(this.amount) < 1000) { if (this.toastTimeout) { window.clearTimeout(this.toastTimeout); } this.toastTimeout = showToast(this.shadow, "Amount too low", "Minimum payment is 1000 sats", "error"); return; } this.setSubmitting(true); try { // Generate the invoice await this.processPayment(); } catch (error) { console.error(error); if (this.toastTimeout) { window.clearTimeout(this.toastTimeout); } this.toastTimeout = showToast(this.shadow, "Error", "Something went wrong: " + error, "error"); } this.setSubmitting(false); } // Process payment using NWC async processPayment() { if (!this.nwcClient) { throw new Error("NWC client not initialized"); } const amountMsat = Number(this.amount) * 1000; const transaction = await this.nwcClient.makeInvoice({ amount: amountMsat, description: this.messageText, metadata: { payer_data: { name: this.senderName, }, }, }); // set bitcoin connect color (basic) const theme = this.getAttribute("theme"); if (theme) { try { const themeObj = JSON.parse(theme); if (themeObj["primary-color"]) { window.document.body.style.setProperty(`--bc-color-brand`, themeObj["primary-color"]); } } catch (e) { console.error("Invalid theme format", e); } } const { setPaid } = launchPaymentModal({ invoice: transaction.invoice, }); const interval = setInterval(async () => { var _a; const updatedTransaction = await ((_a = this.nwcClient) === null || _a === void 0 ? void 0 : _a.lookupInvoice({ payment_hash: transaction.payment_hash, })); if (updatedTransaction === null || updatedTransaction === void 0 ? void 0 : updatedTransaction.preimage) { clearInterval(interval); setPaid({ preimage: updatedTransaction.preimage, }); // Clear message field and reload messages if (this.messageInput) { this.messageInput.value = ""; this.messageText = ""; } await this.loadMessages(); if (this.toastTimeout) { window.clearTimeout(this.toastTimeout); } this.toastTimeout = showToast(this.shadow, "Success", "Message sent successfully"); this.setDialogOpen(false); } }, 1000); } // Load messages from NWC async loadMessages() { if (!this.nwcClient) { return; } this.setLoading(true); try { let offset = 0; const _messages = []; // Fetch transactions in batches while (true) { try { const transactions = await this.nwcClient.listTransactions({ offset, limit: 10, }); if (transactions.transactions.length === 0) { break; } _messages.push(...transactions.transactions.map((transaction) => { var _a, _b; return ({ message: transaction.description, name: (_b = (_a = transaction.metadata) === null || _a === void 0 ? void 0 : _a.payer_data) === null || _b === void 0 ? void 0 : _b.name, amount: Math.floor(transaction.amount / 1000), }); })); offset += transactions.transactions.length; } catch (error) { console.error(error); await new Promise((resolve) => setTimeout(resolve, 1000)); } } // Sort messages by amount (highest first) _messages.sort((a, b) => b.amount - a.amount); this.messages = _messages; this.renderMessages(); } catch (error) { console.error("Failed to load messages", error); this.messages = []; this.renderMessages(); // Show error toast if (this.toastTimeout) { window.clearTimeout(this.toastTimeout); } this.toastTimeout = showToast(this.shadow, "Error", "Failed to load messages: " + error, "error"); } this.setLoading(false); } // Render messages to the DOM renderMessages() { if (!this.messageList) return; // Update top amount for the "Top" button this.topAmount = Math.max(1000, ...(this.messages.map((message) => message.amount + 1) || [])); // Clear the message list this.messageList.innerHTML = ""; // Add each message for (let i = 0; i < this.messages.length; i++) { const message = this.messages[i]; const messageEl = document.createElement("div"); messageEl.innerHTML = ` <div class="card-header"> <h3 class="card-title break-word">${escapeHTML(message.message)}</h3> </div> <div class="card-footer"> <span class="text-muted text-sm">by ${message.name ? escapeHTML(message.name) : "Anonymous"}</span> <div> <span class="badge"> <svg class="zap-icon" viewBox="0 0 24 24"> <polyline points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polyline> </svg> ${formatNumber(message.amount)} </span> </div> </div> ${i !== this.messages.length - 1 ? '<div class="separator"></div>' : ""} `; this.messageList.appendChild(messageEl); } // Show empty state if no messages if (this.messages.length === 0) { const emptyEl = document.createElement("div"); emptyEl.className = "text-muted empty-message"; emptyEl.innerHTML = "No messages yet. Be the first to post!"; this.messageList.appendChild(emptyEl); } } // Set loading state setLoading(isLoading) { if (this.loadingSpinner) { this.loadingSpinner.classList.toggle("hidden", !isLoading); } if (this.centeredLoadingSpinner) { this.centeredLoadingSpinner.classList.toggle("hidden", !isLoading); } } // Set submitting state setSubmitting(isSubmitting) { if (this.confirmButton) { this.confirmButton.disabled = isSubmitting; } if (this.confirmSpinner) { this.confirmSpinner.classList.toggle("hidden", !isSubmitting); } } }