diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aa8094..1a1c982 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Search for contacts by npub or hex pubkey ([erskingardner]) - Copy npub button in settings page([josefinalliende]) - Basic NWC support for paying invoices in messages ([a-mpch], [F3r10], [jgmontoya], [josefinalliende]) +- Show invoice payments as a system message reply rather than as a reaction ([a-mpch], [jgmontoya]) +- Blur QRs and hide pay button for paid invoices in messages ([a-mpch], [jgmontoya], [josefinalliende]) +- Truncate invoice content in messages ([a-mpch], [jgmontoya], [josefinalliende]) ### Changed diff --git a/bun.lockb b/bun.lockb index 4c68d47..90ec4cd 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index a6639aa..f3c7220 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@tauri-apps/plugin-notification": "^2.2.1", "@tauri-apps/plugin-shell": "^2.2.0", "nostr-tools": "^2.10.4", + "qrcode": "^1.5.4", "svelte-gestures": "^5.1.3" }, "devDependencies": { diff --git a/src-tauri/src/commands/payments.rs b/src-tauri/src/commands/payments.rs index 150e499..c5123e1 100644 --- a/src-tauri/src/commands/payments.rs +++ b/src-tauri/src/commands/payments.rs @@ -44,8 +44,8 @@ pub async fn pay_invoice( let preimage = payments::pay_bolt11_invoice(&bolt11, &nwc_uri) .await .map_err(CommandError::from)?; - let message = "⚡".to_string(); - let kind = 7; + let message = "".to_string(); + let kind = 9; let mut final_tags = tags.unwrap_or_default(); final_tags.push(Tag::custom( TagKind::Custom("preimage".into()), diff --git a/src/lib/components/RepliedTo.svelte b/src/lib/components/RepliedTo.svelte index 065acd6..f814f3f 100644 --- a/src/lib/components/RepliedTo.svelte +++ b/src/lib/components/RepliedTo.svelte @@ -22,14 +22,14 @@ onMount(() => { {#if message}
- {#if message.pubkey === $activeAccount?.pubkey} - You + You {:else} - + + + {/if} - - {message.content} + {message.content}
{:else}
diff --git a/src/routes/(app)/chats/[id]/+page.svelte b/src/routes/(app)/chats/[id]/+page.svelte index 05eb280..d3e3807 100644 --- a/src/routes/(app)/chats/[id]/+page.svelte +++ b/src/routes/(app)/chats/[id]/+page.svelte @@ -12,10 +12,12 @@ import { type NEvent, type NostrMlsGroup, NostrMlsGroupType, + type NostrMlsGroupWithRelays, } from "$lib/types/nostr"; import { hexMlsGroupId } from "$lib/utils/group"; import { nameFromMetadata } from "$lib/utils/nostr"; import { formatMessageTime } from "$lib/utils/time"; +import { copyToClipboard } from "$lib/utils/clipboard"; import { invoke } from "@tauri-apps/api/core"; import { type UnlistenFn, listen } from "@tauri-apps/api/event"; import { writeText } from "@tauri-apps/plugin-clipboard-manager"; @@ -25,11 +27,11 @@ import { CheckCircle, CircleDashed, CopySimple, - DotsThree, - Lightning, + DotsThree } from "phosphor-svelte"; import { onDestroy, onMount, tick } from "svelte"; import { type PressCustomEvent, press } from "svelte-gestures"; +import { toDataURL } from 'qrcode'; let unlistenMlsMessageReceived: UnlistenFn; let unlistenMlsMessageProcessed: UnlistenFn; @@ -141,6 +143,23 @@ function handleNewMessage(message: NEvent, replaceTemp: boolean) { messages = [...messages, message].sort((a, b) => a.created_at - b.created_at); scrollToBottom(); } + +function findQTagReplyTo(message: NEvent): string | undefined { + return message.tags.find((t) => t[0] === "q")?.[1] +} + +function doesMessageHaveQTag(message: NEvent): boolean { + return findQTagReplyTo(message) !== undefined; +} + +function findPreimageTagReplyTo(message: NEvent): string | undefined { + return message.tags.find((t) => t[0] === "preimage")?.[1] +} + +function doesMessageHavePreimageTag(message: NEvent): boolean { + return findPreimageTagReplyTo(message) !== undefined; +} + function findBolt11Tag(message: NEvent): string | undefined { return message.tags.find((t) => t[0] === "bolt11")?.[1]; } @@ -275,22 +294,13 @@ async function copyMessage() { } } -async function payInvoice() { +async function payInvoice(message: NEvent) { if (!group) { console.error("no group found"); return; } - if (!selectedMessageId) { - console.error("no message selected"); - return; - } - const message = messages.find((m) => m.id === selectedMessageId); - if (!message) { - console.error("message not found"); - return; - } - - if (!isSelectedMessageBolt11) { + + if (!doesMessageHaveBolt11Tag(message)) { console.error("message is not a bolt11 invoice"); return; } @@ -299,12 +309,11 @@ async function payInvoice() { console.error("Nostr Wallet Connect URI not found"); return; } - + let groupWithRelays: NostrMlsGroupWithRelays = await invoke("get_group", { + groupId: hexMlsGroupId(group.mls_group_id), + }); const invoice = findBolt11Tag(message); - // Filter out tags that are not "e" or "p" (or invalid) - let tags = message.tags.filter((t) => t.length >= 2 && (t[0] === "e" || t[0] === "p")); - // Now add our own tags for the reaction - tags = [...tags, ["e", selectedMessageId], ["p", message.pubkey], ["k", message.kind.toString()]]; + let tags = [["q", message.id, groupWithRelays.relays[0], message.pubkey]]; console.log("Sending payment", tags); invoke("pay_invoice", { group, @@ -328,6 +337,11 @@ async function payInvoice() { }); } +async function copyInvoice(messageId: string) { + const invoice = invoiceDataMap.get(messageId)?.invoice; + if (invoice) await copyToClipboard(invoice, 'bolt11 invoice'); +} + function replyToMessage() { replyToMessageEvent = messages.find((m) => m.id === selectedMessageId); document.getElementById("newMessageInput")?.focus(); @@ -368,6 +382,58 @@ function reactionsForMessage(message: NEvent): { content: string; count: number ); } +function isBolt11Paid(message: NEvent): boolean { + const replies = messages.filter( + (m) => m.kind === 9 && + m.tags.some((t) => t[0] === "q" && t[1] === message.id) && + m.tags.some((t) => t[0] === "preimage") + ) + return replies.length > 0; +} + +let invoiceDataMap = $state(new Map()); + +$effect(() => { + computeInvoices(); +}); + +async function computeInvoices() { + const newMap = new Map(); + + await Promise.all(messages.map(async (message) => { + const bolt11Tag = message.tags.find((t) => t[0] === "bolt11"); + if (bolt11Tag) { + const invoice = bolt11Tag[1]; + const amount = Number(bolt11Tag[2] || 0) / 1000; + try { + const qrCodeUrl = await toDataURL(`lightning:${bolt11Tag[1]}`); + newMap.set(message.id, { invoice, amount, qrCodeUrl }); + } catch (error) { + console.error("Error generating QR code:", error); + newMap.set(message.id, { invoice, amount }); + } + } + })); + + invoiceDataMap = newMap; +} + +function contentToShow(message: NEvent) { + const bolt11_tag = findBolt11Tag(message); + if (!bolt11_tag) { + return message.content; + } + + const invoice = bolt11_tag; + const firstPart = invoice.substring(0, 15); + const lastPart = invoice.substring(invoice.length - 15); + return message.content.replace(invoice, `${firstPart}...${lastPart}`); +} + +function isMyMessage(message: NEvent) { + return message.pubkey === $activeAccount?.pubkey; +} + onDestroy(() => { unlistenMlsMessageProcessed(); unlistenMlsMessageReceived(); @@ -421,20 +487,58 @@ onDestroy(() => { data-message-container data-message-id={message.id} data-is-current-user={message.pubkey === $activeAccount?.pubkey} - class={`relative max-w-[70%] ${!isSingleEmoji(message.content) ? `rounded-lg ${message.pubkey === $activeAccount?.pubkey ? "bg-chat-bg-me text-gray-50 rounded-br" : "bg-chat-bg-other text-gray-50 rounded-bl"} p-3` : ''} ${showMessageMenu && message.id === selectedMessageId ? 'relative z-20' : ''}`} + class={`relative max-w-[70%] ${doesMessageHavePreimageTag(message) ? "bg-opacity-10" : ""} ${!isSingleEmoji(message.content) ? `rounded-lg ${message.pubkey === $activeAccount?.pubkey ? `bg-chat-bg-me text-gray-50 rounded-br` : `bg-chat-bg-other text-gray-50 rounded-bl`} p-3` : ''} ${showMessageMenu && message.id === selectedMessageId ? 'relative z-20' : ''}`} > - {#if message.tags.find((t) => t[0] === "q")?.[1]} - t[0] === "q")?.[1]} /> + {#if doesMessageHaveQTag(message)} + {/if} -
-
+
+
{#if message.content.trim().length > 0} - {message.content} + {contentToShow(message)} + {:else if doesMessageHavePreimageTag(message)} +
+ ⚡️Invoice paid⚡️ +
{:else} No message content {/if} + {#if invoiceDataMap.has(message.id)} +
+
+ QR Code + {#if isBolt11Paid(message)} + + {/if} +
+
+ + {#if accountHasNostrWalletConnectUri && !isBolt11Paid(message)} + + {/if} +
+
+ {/if}
-
+
{#if message.id !== "temp"} {:else} @@ -500,9 +604,6 @@ onDestroy(() => {
- {#if isSelectedMessageBolt11 && accountHasNostrWalletConnectUri} - - {/if}
@@ -532,7 +633,7 @@ onDestroy(() => { content: ''; position: absolute; inset: -1px; - background: linear-gradient(90deg, #ff00ea 0%, #ad00ff 100%); + background: linear-gradient(90deg, #f97316 0%, #ea580c 100%); z-index: -1; opacity: 0.15; filter: blur(8px); @@ -540,7 +641,7 @@ onDestroy(() => { } .glow-button:hover { - background: rgba(173, 0, 255, 0.2); + background: rgba(21, 132, 79, 0.2); } /* Ensure immediate visibility state change */