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}
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)}
+
+
+
?.qrCodeUrl})
+ {#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 */