import { Card, CardContent, CardFooter, CardHeader, CardTitle, } from "src/components/ui/card"; import { NWCClient } from "@getalby/sdk/nwc"; import dayjs from "dayjs"; import { ChevronUpIcon, ZapIcon } from "lucide-react"; import React from "react"; import { toast } from "sonner"; import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; import Loading from "src/components/Loading"; import { Badge } from "src/components/ui/badge"; import { Button } from "src/components/ui/button"; import { LoadingButton } from "src/components/ui/custom/loading-button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "src/components/ui/dialog"; import { Input } from "src/components/ui/input"; import { Label } from "src/components/ui/label"; import { Separator } from "src/components/ui/separator"; import { Textarea } from "src/components/ui/textarea"; import { PayInvoiceResponse } from "src/types"; import { request } from "src/utils/request"; // Must be a sub-wallet connection with only make invoice and list transactions permissions! const LIGHTNING_MESSAGEBOARD_NWC_URL = import.meta.env.VITE_LIGHTNING_MESSAGEBOARD_NWC_URL || "nostr+walletconnect://31758cb11d8060fa87ea955808dc22e3602aad7390717edd56dbbbd136c85a9b?relay=wss://relay.getalby.com&relay=wss://relay2.getalby.com&secret=dce4d879ca8d875b0dc38f98425829eff71a5d213db9d5d423bf284fa75efc80"; type Message = { name?: string; message: string; amountSat: number; created_at: number; }; type TabType = "latest" | "top"; let nwcClient: NWCClient | undefined; function getNWCClient(): NWCClient { if (!nwcClient) { nwcClient = new NWCClient({ nostrWalletConnectUrl: LIGHTNING_MESSAGEBOARD_NWC_URL, }); } return nwcClient; } function getSortedMessages(messages: Message[], tab: TabType): Message[] { if (tab === "latest") { return [...messages].sort((a, b) => b.created_at - a.created_at); } else { return [...messages].sort((a, b) => b.amountSat - a.amountSat); } } export function LightningMessageboardWidget() { const [messageText, setMessageText] = React.useState(""); const [senderName, setSenderName] = React.useState(""); const [amountSat, setAmountSat] = React.useState(""); const [messages, setMessages] = React.useState(); const [isLoading, setLoading] = React.useState(false); const [isSubmitting, setSubmitting] = React.useState(false); const [dialogOpen, setDialogOpen] = React.useState(false); const [isOpen, setOpen] = React.useState(false); const [currentTab, setCurrentTab] = React.useState("latest"); const loadMessages = React.useCallback(() => { (async () => { setLoading(true); let offset = 0; const _messages: Message[] = []; while (true) { try { const transactions = await getNWCClient().listTransactions({ offset, limit: 10, }); if (transactions.transactions.length === 0) { break; } const newMessages = transactions.transactions.map((transaction) => ({ created_at: transaction.created_at, message: transaction.description, name: ( transaction.metadata as | { payer_data?: { name?: string } } | undefined )?.payer_data?.name as string | undefined, amountSat: Math.floor(transaction.amount / 1000), })); _messages.push(...newMessages); // Update messages incrementally as they load setMessages((prevMessages) => { return [...(prevMessages || []), ...newMessages]; }); offset += transactions.transactions.length; } catch (error) { console.error(error); await new Promise((resolve) => setTimeout(resolve, 1000)); } } setLoading(false); })(); }, []); const hasLoadedMessages = !!messages; React.useEffect(() => { if (isOpen && !hasLoadedMessages) { loadMessages(); } }, [hasLoadedMessages, isOpen, loadMessages]); const sortedMessages = React.useMemo( () => getSortedMessages(messages || [], currentTab), [currentTab, messages] ); function handleSubmitOpenDialog(e: React.FormEvent) { e.preventDefault(); setDialogOpen(true); } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (+amountSat < 1000) { toast.error("Amount too low", { description: "Minimum payment is 1000 sats", }); return; } const amountMsat = +amountSat * 1000; setSubmitting(true); try { const transaction = await getNWCClient().makeInvoice({ amount: amountMsat, description: messageText, metadata: { payer_data: { name: senderName, }, }, }); const payInvoiceResponse = await request( `/api/payments/${transaction.invoice}`, { method: "POST", } ); if (!payInvoiceResponse?.preimage) { throw new Error("No preimage in response"); } setMessageText(""); loadMessages(); toast("Successfully sent message"); setDialogOpen(false); } catch (error) { console.error(error); toast.error("Something went wrong", { description: "" + error, }); } setSubmitting(false); } const topPlaceSat = Math.max( 1000, ...(messages?.map((message) => message.amountSat + 1) || []) ); return ( <>
Lightning Messageboard{isLoading && }
{isOpen && (
{sortedMessages.map((message, index) => (
{message.message} by{" "} {message.name || "Anonymous"}{" "} {dayjs(message.created_at * 1000).fromNow()}
{index !== sortedMessages.length - 1 && }
))}
setMessageText(e.target.value)} />
)}
Post Message Pay to post on the Alby Hub message board. The messages with the highest number of satoshis will be shown first.
setSenderName(e.target.value)} maxLength={20} autoFocus />
setAmountSat(e.target.value)} />