From fdc035a8c5a64c47169c495d7f81bd0ab7643fb9 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Mon, 25 Aug 2025 19:24:16 -0500 Subject: Add direct messages page The CSS isn't great right now, but you can go back and forth! I'll make it look nice another day. --- src/routes/(app)/[username]/(profile)/+layout.svelte | 5 +++++ src/routes/(app)/dm/[recipient]/+page.server.ts | 5 +++++ src/routes/(app)/dm/[recipient]/+page.svelte | 20 ++++++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 src/routes/(app)/dm/[recipient]/+page.server.ts create mode 100644 src/routes/(app)/dm/[recipient]/+page.svelte (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/[username]/(profile)/+layout.svelte b/src/routes/(app)/[username]/(profile)/+layout.svelte index b384344e..2e6657bd 100644 --- a/src/routes/(app)/[username]/(profile)/+layout.svelte +++ b/src/routes/(app)/[username]/(profile)/+layout.svelte @@ -140,6 +140,11 @@ + {:else} + + + {/if} diff --git a/src/routes/(app)/dm/[recipient]/+page.server.ts b/src/routes/(app)/dm/[recipient]/+page.server.ts new file mode 100644 index 00000000..e202d8b4 --- /dev/null +++ b/src/routes/(app)/dm/[recipient]/+page.server.ts @@ -0,0 +1,5 @@ +export function load({ params }) { + return { + recipient: params.recipient + }; +} diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte new file mode 100644 index 00000000..fd539d12 --- /dev/null +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -0,0 +1,20 @@ + + + +
+ + + + +
+

Nostr Direct Messages

+

You are sending messages to {data.recipient}.

+
+
-- cgit v1.2.3 From f7886a3bd35a2ec47ac7f66bf89cf59d1221eb56 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 26 Aug 2025 08:31:34 -0500 Subject: Add basic messaging UI --- src/routes/(app)/[username]/(profile)/+layout.svelte | 6 +++++- src/routes/(app)/dm/[recipient]/+page.svelte | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/[username]/(profile)/+layout.svelte b/src/routes/(app)/[username]/(profile)/+layout.svelte index 2e6657bd..8ce4150c 100644 --- a/src/routes/(app)/[username]/(profile)/+layout.svelte +++ b/src/routes/(app)/[username]/(profile)/+layout.svelte @@ -141,7 +141,11 @@ > {:else} - + diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index fd539d12..78146494 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -3,7 +3,6 @@ import Icon from "$comp/Icon.svelte"; - -- cgit v1.2.3 From 78a7f50c06e8c0c5cf0dc7ae5dcf520cc030bcc0 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 26 Aug 2025 10:27:31 -0500 Subject: Add more styling to DM page --- src/routes/(app)/dm/[recipient]/+page.svelte | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 78146494..c65931f5 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -13,8 +13,10 @@
-

Nostr Direct Messages

-

You are sending messages to {data.recipient}.

+

Direct Messages

+

You are sending nostr messages to {data.recipient}.

-- cgit v1.2.3 From 8e9ae1dcaccf9d58b29865f296c01c5ace1f581b Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 26 Aug 2025 10:31:06 -0500 Subject: Determine DM recipient's pubkey --- src/routes/(app)/dm/[recipient]/+page.server.ts | 9 ++++---- src/routes/(app)/dm/[recipient]/+page.svelte | 28 +++++++++++++------------ 2 files changed, 20 insertions(+), 17 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.server.ts b/src/routes/(app)/dm/[recipient]/+page.server.ts index e202d8b4..0a320137 100644 --- a/src/routes/(app)/dm/[recipient]/+page.server.ts +++ b/src/routes/(app)/dm/[recipient]/+page.server.ts @@ -1,5 +1,6 @@ -export function load({ params }) { - return { - recipient: params.recipient - }; +import { get } from "$lib/utils"; + +export async function load({ params }) { + const recipient = await get(`/users/${params.recipient}`); + return { recipient }; } diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index c65931f5..aff22eed 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -1,22 +1,24 @@
- - - + + + -
-

Direct Messages

-

You are sending nostr messages to {data.recipient}.

+
+

Direct Messages

+

Testing Message: You are sending nostr messages to {recipient.username}. Their pubkey is {recipient.pubkey}.

-- cgit v1.2.3 From 0df147447bdc08069e1e7b1cbfe116dbabf00233 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 26 Aug 2025 16:17:57 -0500 Subject: Add testing NIP-17 implementation --- src/routes/(app)/dm/[recipient]/+page.svelte | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index aff22eed..ccfcbf80 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -1,8 +1,23 @@
@@ -20,8 +35,12 @@ >Direct Messages

Testing Message: You are sending nostr messages to {recipient.username}. Their pubkey is {recipient.pubkey}.

- + + + + + - -
+ +
-- cgit v1.2.3 From e7dc3dc7c42803f6c5f020055a00af5f9d89d8a9 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 26 Aug 2025 20:14:08 -0500 Subject: Add improved NIP-17 code This is based on an audited NIP-44 implementation. --- src/lib/nip17.ts | 59 ++++++++++++ src/lib/nip44.ts | 135 +++++++++++++++++++++++++++ src/routes/(app)/dm/[recipient]/+page.svelte | 4 +- 3 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 src/lib/nip17.ts create mode 100644 src/lib/nip44.ts (limited to 'src/routes/(app)') diff --git a/src/lib/nip17.ts b/src/lib/nip17.ts new file mode 100644 index 00000000..66f49639 --- /dev/null +++ b/src/lib/nip17.ts @@ -0,0 +1,59 @@ +import { bytesToHex } from '@noble/hashes/utils'; +import { finalizeEvent, getEventHash, generateSecretKey, getPublicKey } from 'nostr-tools/pure'; +import { encrypt, u } from '$lib/nip44'; + +const TWO_DAYS = 2 * 24 * 60 * 60; +const randomTimestamp = () => Math.floor( + Date.now() / 1000 - Math.random() * TWO_DAYS); + +export const createNIP17Message = (text: string, senderSK: Uint8Array, receiverPK: string) => { + const rumour = createRumour(text, senderSK, receiverPK); + const sealed = sealRumour(rumour, senderSK, receiverPK); + const wrapped = giftWrap(sealed, receiverPK); + return wrapped; +} + +const createRumour = (text: string, senderSK: Uint8Array, receiverPK: string) => { + const rumour = { + kind: 14, // rumour + created_at: Math.floor(Date.now() / 1000), + tags: [["p", receiverPK]], + pubkey: getPublicKey(senderSK), + content: text + }; + rumour.id = getEventHash(rumour); + return rumour; +} + +const sealRumour = (rumour: object, senderSK: Uint8Array, receiverPK: string) => { + const senderSKString = bytesToHex(senderSK); + const conversationKey = u.getConversationKey(senderSKString, receiverPK); + const encryptedRumour = encrypt(JSON.stringify(rumour), conversationKey); + + const sealEvent = { + kind: 13, // seal + created_at: randomTimestamp(), + tags: [], + pubkey: getPublicKey(senderSK), + content: encryptedRumour + } + + return finalizeEvent(sealEvent, senderSK); +} + +const giftWrap = (event: object, receiverPK: string) => { + const secretKey = generateSecretKey(); + const SKString = bytesToHex(secretKey); + const conversationKey = u.getConversationKey(secretKey, receiverPK); + const encryptedEvent = encrypt(JSON.stringify(event), conversationKey); + + const giftWrapEvent = { + kind: 1059, // gift wrap + created_at: randomTimestamp(), + tags: [["p", receiverPK]], + pubkey: getPublicKey(secretKey), + content: encryptedEvent + } + + return finalizeEvent(giftWrapEvent, secretKey); +} diff --git a/src/lib/nip44.ts b/src/lib/nip44.ts new file mode 100644 index 00000000..313d6f6b --- /dev/null +++ b/src/lib/nip44.ts @@ -0,0 +1,135 @@ +// Copied from this public-domain code: +// https://github.com/paulmillr/nip44/blob/main/javascript/index.ts +// Modified slightly to deal with a dependency issue + +import { chacha20 } from '@noble/ciphers/chacha'; +import { equalBytes } from '@noble/ciphers/utils'; +import { secp256k1 } from '@noble/curves/secp256k1'; +import { extract as hkdf_extract, expand as hkdf_expand } from '@noble/hashes/hkdf'; +import { hmac } from '@noble/hashes/hmac'; +import { sha256 } from '@noble/hashes/sha256'; +import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils'; +import { base64 } from '@scure/base'; + +declare const TextDecoder: any; + +const decoder = new TextDecoder(); +export const u = { + minPlaintextSize: 0x0001, // 1b msg => padded to 32b + maxPlaintextSize: 0xffff, // 65535 (64kb-1) => padded to 64kb + + utf8Encode: utf8ToBytes, + utf8Decode(bytes: Uint8Array) { + return decoder.decode(bytes); + }, + + getConversationKey(privkeyA: string, pubkeyB: string): Uint8Array { + const sharedX = secp256k1.getSharedSecret(privkeyA, '02' + pubkeyB).subarray(1, 33); + return hkdf_extract(sha256, sharedX, 'nip44-v2'); + }, + + getMessageKeys(conversationKey: Uint8Array, nonce: Uint8Array) { + // ensureBytes(conversationKey, 32); + // ensureBytes(nonce, 32); + const keys = hkdf_expand(sha256, conversationKey, nonce, 76); + return { + chacha_key: keys.subarray(0, 32), + chacha_nonce: keys.subarray(32, 44), + hmac_key: keys.subarray(44, 76), + }; + }, + + calcPaddedLen(len: number): number { + if (!Number.isSafeInteger(len) || len < 1) throw new Error('expected positive integer'); + if (len <= 32) return 32; + const nextPower = 1 << (Math.floor(Math.log2(len - 1)) + 1); + const chunk = nextPower <= 256 ? 32 : nextPower / 8; + return chunk * (Math.floor((len - 1) / chunk) + 1); + }, + + writeU16BE(num: number) { + if (!Number.isSafeInteger(num) || num < u.minPlaintextSize || num > u.maxPlaintextSize) + throw new Error('invalid plaintext size: must be between 1 and 65535 bytes'); + const arr = new Uint8Array(2); + new DataView(arr.buffer).setUint16(0, num, false); + return arr; + }, + + pad(plaintext: string): Uint8Array { + const unpadded = u.utf8Encode(plaintext); + const unpaddedLen = unpadded.length; + const prefix = u.writeU16BE(unpaddedLen); + const suffix = new Uint8Array(u.calcPaddedLen(unpaddedLen) - unpaddedLen); + return concatBytes(prefix, unpadded, suffix); + }, + + unpad(padded: Uint8Array): string { + const unpaddedLen = new DataView(padded.buffer).getUint16(0); + const unpadded = padded.subarray(2, 2 + unpaddedLen); + if ( + unpaddedLen < u.minPlaintextSize || + unpaddedLen > u.maxPlaintextSize || + unpadded.length !== unpaddedLen || + padded.length !== 2 + u.calcPaddedLen(unpaddedLen) + ) + throw new Error('invalid padding'); + return u.utf8Decode(unpadded); + }, + + hmacAad(key: Uint8Array, message: Uint8Array, aad: Uint8Array) { + if (aad.length !== 32) throw new Error('AAD associated data must be 32 bytes'); + const combined = concatBytes(aad, message); + return hmac(sha256, key, combined); + }, + + // metadata: always 65b (version: 1b, nonce: 32b, max: 32b) + // plaintext: 1b to 0xffff + // padded plaintext: 32b to 0xffff + // ciphertext: 32b+2 to 0xffff+2 + // raw payload: 99 (65+32+2) to 65603 (65+0xffff+2) + // compressed payload (base64): 132b to 87472b + decodePayload(payload: string) { + if (typeof payload !== 'string') throw new Error('payload must be a valid string'); + const plen = payload.length; + if (plen < 132 || plen > 87472) throw new Error('invalid payload length: ' + plen); + if (payload[0] === '#') throw new Error('unknown encryption version'); + let data: Uint8Array; + try { + data = base64.decode(payload); + } catch (error) { + throw new Error('invalid base64: ' + (error as any).message); + } + const dlen = data.length; + if (dlen < 99 || dlen > 65603) throw new Error('invalid data length: ' + dlen); + const vers = data[0]; + if (vers !== 2) throw new Error('unknown encryption version ' + vers); + return { + nonce: data.subarray(1, 33), + ciphertext: data.subarray(33, -32), + mac: data.subarray(-32), + }; + }, +}; + +export function encrypt(plaintext: string, conversationKey: Uint8Array, nonce = randomBytes(32)): string { + const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys(conversationKey, nonce); + const padded = u.pad(plaintext); + const ciphertext = chacha20(chacha_key, chacha_nonce, padded); + const mac = u.hmacAad(hmac_key, ciphertext, nonce); + return base64.encode(concatBytes(new Uint8Array([2]), nonce, ciphertext, mac)); +} + +export function decrypt(payload: string, conversationKey: Uint8Array): string { + const { nonce, ciphertext, mac } = u.decodePayload(payload); + const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys(conversationKey, nonce); + const calculatedMac = u.hmacAad(hmac_key, ciphertext, nonce); + if (!equalBytes(calculatedMac, mac)) throw new Error('invalid MAC'); + const padded = chacha20(chacha_key, chacha_nonce, ciphertext); + return u.unpad(padded); +} + +export const v2 = { + utils: u, + encrypt, + decrypt, +}; diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index ccfcbf80..7832df8d 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -2,6 +2,7 @@ import Icon from "$comp/Icon.svelte"; import { hexToBytes } from '@noble/hashes/utils'; import * as nip17 from 'nostr-tools/nip17'; + import { createNIP17Message } from '$lib/nip17'; let { data } = $props(); const { recipient } = data; @@ -11,8 +12,7 @@ const btnCreateMessage = () => { try { const skBytes = hexToBytes(senderSK); - const recipientData = { publicKey: recipient.pubkey }; - const event = nip17.wrapEvent(skBytes, recipientData, text); + const event = createNIP17Message(text, skBytes, recipient.pubkey); console.log(JSON.stringify(event)); } catch (error) { console.log(error); -- cgit v1.2.3 From 2efc8bb7e543102673aa8cee80ec4f3681c2e51d Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 28 Aug 2025 16:54:56 -0500 Subject: Allow temporary override of recipient PK --- src/routes/(app)/dm/[recipient]/+page.svelte | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 7832df8d..0486d00f 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -7,12 +7,13 @@ let { data } = $props(); const { recipient } = data; - let senderSK, text; + let senderSK, overridePK, text; const btnCreateMessage = () => { try { + const recipientPK = overridePK || recipient.pubkey; const skBytes = hexToBytes(senderSK); - const event = createNIP17Message(text, skBytes, recipient.pubkey); + const event = createNIP17Message(text, skBytes, recipientPK); console.log(JSON.stringify(event)); } catch (error) { console.log(error); @@ -38,6 +39,9 @@ + + + -- cgit v1.2.3 From cb5d64ef82ab4acd636d024234d2d3bdd5959b0b Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 4 Sep 2025 19:29:36 -0500 Subject: Try to get user private key --- src/routes/(app)/dm/[recipient]/+page.svelte | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 0486d00f..b4cb1459 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -1,14 +1,21 @@
-- cgit v1.2.3 From fd098a90952269db708f4d2d21de085c968e835e Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Fri, 5 Sep 2025 19:43:20 -0500 Subject: Remove need for explicit key entry If window.nostr is available, I don't need the sender's SK, I can do everything with its functions. Otherwise, I'll ask for the SK using getPrivateKey(). The override public key was only for testing, but I still don't need it because I can get the users' private keys from the settings tab. --- src/lib/nip17.ts | 30 +++++++++++---------- src/routes/(app)/dm/[recipient]/+page.svelte | 40 +++++++++------------------- 2 files changed, 29 insertions(+), 41 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/lib/nip17.ts b/src/lib/nip17.ts index e0046d47..6a9888b6 100644 --- a/src/lib/nip17.ts +++ b/src/lib/nip17.ts @@ -6,31 +6,33 @@ const TWO_DAYS = 2 * 24 * 60 * 60; const randomTimestamp = () => Math.floor( Date.now() / 1000 - Math.random() * TWO_DAYS); -export const createNIP17Message = async (text: string, senderSK: Uint8Array, receiverPK: string) => { - const rumour = createRumour(text, senderSK, receiverPK); - let sealed; - if (window.nostr) { - sealed = await sealRumourNip07(rumour, receiverPK); - } else { - sealed = sealRumour(rumour, senderSK, receiverPK); - } - const wrapped = giftWrap(sealed, receiverPK); - return wrapped; +// Create a NIP-17 message using a provided sender secret key. +export const createNIP17MessageSK = (text: string, senderSK: Uint8Array, receiverPK: string) => { + const rumour = createRumour(text, getPublicKey(senderSK), receiverPK); + const sealed = sealRumourSK(rumour, senderSK, receiverPK); + return giftWrap(sealed, receiverPK); +} + +// Create a NIP-17 message using NIP-07, requiring no sender secret key. +export const createNIP17MessageNIP07 = async (text: string, senderPK: string, receiverPK: string) => { + const rumour = createRumour(text, senderPK, receiverPK); + const sealed = await sealRumourNip07(rumour, receiverPK); + return giftWrap(sealed, receiverPK); } -const createRumour = (text: string, senderSK: Uint8Array, receiverPK: string) => { +const createRumour = (text: string, senderPK: string, receiverPK: string) => { const rumour = { kind: 14, // rumour created_at: Math.floor(Date.now() / 1000), tags: [["p", receiverPK]], - pubkey: getPublicKey(senderSK), + pubkey: senderPK, content: text }; - rumour.id = getEventHash(rumour); + rumour.id = getEventHash(rumour); // do NOT sign rumours! return rumour; } -const sealRumour = (rumour: object, senderSK: Uint8Array, receiverPK: string) => { +const sealRumourSK = (rumour: object, senderSK: Uint8Array, receiverPK: string) => { const senderSKString = bytesToHex(senderSK); const conversationKey = u.getConversationKey(senderSKString, receiverPK); const encryptedRumour = encrypt(JSON.stringify(rumour), conversationKey); diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 33edfcc6..446a1068 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -2,33 +2,26 @@ import Icon from "$comp/Icon.svelte"; import { hexToBytes, bytesToHex } from '@noble/hashes/utils'; import * as nip17 from 'nostr-tools/nip17'; - import { createNIP17Message } from '$lib/nip17'; + import { createNIP17MessageSK, createNIP17MessageNIP07 } from '$lib/nip17'; import { sign, getPrivateKey } from '$lib/nostr'; let { data } = $props(); const { user, recipient } = data; - let senderSK = $state(""); - let overridePK = $state(""); let text = $state(""); - getPrivateKey(user) - .then(key => senderSK = bytesToHex(key)) - .catch(console.log); - - const btnCreateMessage = () => { - try { - const recipientPK = overridePK || recipient.pubkey; - const skBytes = hexToBytes(senderSK); - const event = createNIP17Message(text, skBytes, recipientPK); - event.then(displayEvent); - } catch (error) { - console.log(error); + const btnCreateMessage = async () => { + let event; + if (window.nostr) { + event = await createNIP17MessageNIP07( + text, user.pubkey, recipient.pubkey); + } else { + const sk = await getPrivateKey(user); + event = createNIP17MessageSK(text, sk, recipient.pubkey); } - } - const displayEvent = (event: object) => { - console.log(JSON.stringify(event)); + // TODO send event to relays instead of displaying it + text = JSON.stringify(event); } @@ -45,17 +38,10 @@

Direct Messages

-

Testing Message: You are sending nostr messages to {recipient.username}. Their pubkey is {recipient.pubkey}.

- - - - - - +

You are sending nostr messages to {recipient.username}.

- - +
-- cgit v1.2.3 From 19646ca06597a8e4d14a510208f1519c09cfed3f Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 26 Aug 2025 21:53:38 -0700 Subject: fix autopay with tip logic --- src/routes/(app)/invoice/[id]/tip/+page.server.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/invoice/[id]/tip/+page.server.ts b/src/routes/(app)/invoice/[id]/tip/+page.server.ts index 23e3a07c..851d3f1b 100644 --- a/src/routes/(app)/invoice/[id]/tip/+page.server.ts +++ b/src/routes/(app)/invoice/[id]/tip/+page.server.ts @@ -19,14 +19,13 @@ export async function load({ cookies, params, parent }) { if (user.tip > 0) { invoice.tip = Math.round(invoice.amount * (user.tip / 100)); invoice = await post(`/invoice/${id}`, { invoice }, auth(cookies)); - } else { - throw new Error("tip"); + p = await post("/payments", { ...invoice, pin }, auth(cookies)); } + } else { + p = await post("/payments", { ...invoice, pin }, auth(cookies)); } - p = await post("/payments", { ...invoice, pin }, auth(cookies)); } catch (e) { const { message } = e as Error; - if (message === "tip") redirect(307, `/invoice/${id}/tip`); fail(400, { message }); } if (p) redirect(307, `/sent/${p.id}`); -- cgit v1.2.3 From 2669f543872b2141ec8e3ca127603b41472a0614 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 3 Sep 2025 00:54:45 -0700 Subject: always show lnurl button --- src/routes/(app)/[username]/accepted/[text]/+page.svelte | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/[username]/accepted/[text]/+page.svelte b/src/routes/(app)/[username]/accepted/[text]/+page.svelte index 95b6cc3e..1483a294 100644 --- a/src/routes/(app)/[username]/accepted/[text]/+page.svelte +++ b/src/routes/(app)/[username]/accepted/[text]/+page.svelte @@ -50,10 +50,8 @@
- {#if nfc} - - {/if} + -- cgit v1.2.3 From 3d5964347df0d34df8cad5846da843b626d6b3a6 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 9 Sep 2025 09:18:11 -0700 Subject: check for user in invoice route --- bun.lockb | Bin 292357 -> 292349 bytes src/routes/(app)/invoice/[id]/+layout.server.ts | 4 ++-- src/routes/(app)/invoice/[id]/+page.svelte | 20 +++++++++++--------- 3 files changed, 13 insertions(+), 11 deletions(-) (limited to 'src/routes/(app)') diff --git a/bun.lockb b/bun.lockb index 8dd85e14..e557130e 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/src/routes/(app)/invoice/[id]/+layout.server.ts b/src/routes/(app)/invoice/[id]/+layout.server.ts index d6ab979e..df8693be 100644 --- a/src/routes/(app)/invoice/[id]/+layout.server.ts +++ b/src/routes/(app)/invoice/[id]/+layout.server.ts @@ -19,7 +19,7 @@ export async function load({ cookies, depends, params, url, parent }) { (!amount && (pending || received)) || (amount > 0 && (pending >= amount || received >= amount)); if (paid && !url.pathname.endsWith("paid")) { - if (invoice.uid === user.id) + if (invoice.uid === user?.id) redirect(307, `/invoice/${id}/paid` + (options ? "?options=true" : "")); else redirect(307, `/pay/${subject.username}/${invoice.amount}`); } @@ -28,7 +28,7 @@ export async function load({ cookies, depends, params, url, parent }) { if ( user && - invoice.uid !== user.id && + invoice.uid !== user?.id && !(url.pathname.includes("tip") || url.pathname.includes("memo")) && !options ) { diff --git a/src/routes/(app)/invoice/[id]/+page.svelte b/src/routes/(app)/invoice/[id]/+page.svelte index 0cb5aeac..29dd815d 100644 --- a/src/routes/(app)/invoice/[id]/+page.svelte +++ b/src/routes/(app)/invoice/[id]/+page.svelte @@ -56,16 +56,18 @@ let subbed = false; + $effect(() => { + if (!browser) return; + last.subscribe((v) => { + if (!v || subbed) return; + subbed = true; + send("subscribe", invoice); + }); + }); + onMount(async () => { - if (browser) { - last.subscribe((v) => { - if (!v || subbed) return; - subbed = true; - send("subscribe", invoice); - }); - - if ($amountPrompt && !amount) toggleAmount(); - } + if (!browser) return; + if ($amountPrompt && !amount) toggleAmount(); }); let update = async () => { -- cgit v1.2.3 From 301c5345e31ce7432e6c0f48aa11133881281cdb Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 9 Sep 2025 12:38:40 -0700 Subject: redirect to paid route --- src/components/Numpad.svelte | 1 - src/routes/(app)/invoice/[id]/+layout.server.ts | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/components/Numpad.svelte b/src/components/Numpad.svelte index 0290b9e2..c321e0bc 100644 --- a/src/components/Numpad.svelte +++ b/src/components/Numpad.svelte @@ -63,7 +63,6 @@ return `${whole}${dec}${cents}`; } - // --- NEW: normalize sats to remove leading zeros but keep single "0" function normalizeSats(str) { const digits = (str ?? "").replace(/[^\d]/g, ""); const noLead = digits.replace(/^0+(?=\d)/, ""); diff --git a/src/routes/(app)/invoice/[id]/+layout.server.ts b/src/routes/(app)/invoice/[id]/+layout.server.ts index df8693be..ab41dddc 100644 --- a/src/routes/(app)/invoice/[id]/+layout.server.ts +++ b/src/routes/(app)/invoice/[id]/+layout.server.ts @@ -19,9 +19,7 @@ export async function load({ cookies, depends, params, url, parent }) { (!amount && (pending || received)) || (amount > 0 && (pending >= amount || received >= amount)); if (paid && !url.pathname.endsWith("paid")) { - if (invoice.uid === user?.id) - redirect(307, `/invoice/${id}/paid` + (options ? "?options=true" : "")); - else redirect(307, `/pay/${subject.username}/${invoice.amount}`); + redirect(307, `/invoice/${id}/paid` + (options ? "?options=true" : "")); } subject = invoice.user; @@ -34,7 +32,9 @@ export async function load({ cookies, depends, params, url, parent }) { ) { if (invoice.prompt && invoice.tip === null) redirect(307, `/invoice/${id}/tip`); - else redirect(307, `/send/invoice/${id}`); + else { + redirect(307, `/send/invoice/${id}`); + } } } -- cgit v1.2.3 From e5e21d3d7027e0cda4448ed345a5b1889578e6ca Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 9 Sep 2025 14:58:59 -0700 Subject: daisy fixes --- src/app.css | 99 +++++++++++--------------- src/routes/(app)/settings/nostr/+page.svelte | 38 +++++----- src/routes/(app)/settings/profile/+page.svelte | 4 +- src/routes/(static)/login/+page.svelte | 2 +- src/routes/(static)/register/+page.svelte | 2 +- 5 files changed, 66 insertions(+), 79 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/app.css b/src/app.css index c04131bd..fca47b23 100644 --- a/src/app.css +++ b/src/app.css @@ -1,50 +1,59 @@ +/* 1) Load Tailwind first */ @import "tailwindcss"; @tailwind base; @tailwind components; @tailwind utilities; - -@font-face { - font-family: "PublicSans"; - src: url("/fonts/Public_Sans/static/PublicSans-Regular.ttf") - format("truetype"); +/* 2) Register DaisyUI themes */ +@plugin "daisyui" { + themes: lofi --default, black --prefersdark; } -html { - font-family: PublicSans, sans-serif; +@plugin "daisyui/theme" { + name: "lofi"; + default: true; + --gradient: #C3CFE2; + --color-error: #b91c1c; } -::selection { - color: black; - background: #eaf2ff; +@plugin "daisyui/theme" { + name: "black"; + prefersdark: true; + --gradient: #C3CFE2; + --color-secondary: #CCC; + --color-error: #f87171; } -:fullscreen { - background-color: white; -} +/* 3) Tell Tailwind which layers we’ll @apply from */ +@reference utilities; +@reference components; -input:not([type="range"]) { - @apply input +/* 4) Now your custom CSS that uses @apply */ +.btn-menu { + @apply flex justify-center items-center bg-base-100 p-2 rounded-full + w-12 h-12 sm:w-16 sm:h-16 drop-shadow-xl; } -.input { - @apply input-lg input-primary; +@font-face { + font-family: "PublicSans"; + src: url("/fonts/Public_Sans/static/PublicSans-Regular.ttf") format("truetype"); } -textarea { - @apply textarea textarea-lg textarea-primary; -} +html { font-family: PublicSans, sans-serif; } -select { - @apply select select-lg select-primary; -} +::selection { color: black; background: #eaf2ff; } +:fullscreen { background-color: white; } -input:not([type="range"]), textarea, select, .input { - @apply w-full rounded-2xl; -} +/* Note: @apply-ing DaisyUI component classes requires components reference */ +.input { @apply input-lg input-primary h-16 px-4; } + +/* If you want to *turn* native inputs into DaisyUI inputs via @apply: */ +input:not([type="range"]) { @apply input input-lg input-primary h-16 px-4; } +textarea { @apply textarea textarea-lg textarea-primary py-4; } +select { @apply select select-lg select-primary h-16 px-4; } -textarea:focus, textarea:focus-within, .input:focus, .input:focus-within { - @apply outline-offset-0; +input:not([type="range"]), textarea, select, .input { + @apply rounded-2xl w-full; } input.clean, textarea.clean { @@ -56,33 +65,11 @@ input.clean, textarea.clean { box-shadow: none !important; } -.btn { - @apply rounded-2xl btn-lg btn-block outline-none -} - -@plugin "daisyui" { - themes: light --default, dark --prefersdark; -} - -@plugin "daisyui/theme" { - name: "light"; - default: true; - - --gradient: #C3CFE2; - --color-error: #b91c1c; - --color-primary: #000; - --color-secondary: #111; -} - -@plugin "daisyui/theme" { - name: "dark"; - prefersdark: true; - - --gradient: #C3CFE2; - --color-primary: #FFF; - --color-secondary: #CCC; - --color-error: #f87171; -} +button { cursor: pointer !important; } +.btn { @apply rounded-2xl btn-lg btn-block outline-none h-16 cursor-pointer; } -:root, [data-theme="dark"] { +@layer components { + :where(input, textarea, select, .input, .textarea, .select) { + outline-offset: 0 !important; + } } diff --git a/src/routes/(app)/settings/nostr/+page.svelte b/src/routes/(app)/settings/nostr/+page.svelte index 421b590a..9129e8f3 100644 --- a/src/routes/(app)/settings/nostr/+page.svelte +++ b/src/routes/(app)/settings/nostr/+page.svelte @@ -163,25 +163,25 @@
{$t("user.nostrPubkey")}
-
- -
- - - - + +
+
+ + + + +
diff --git a/src/routes/(app)/settings/profile/+page.svelte b/src/routes/(app)/settings/profile/+page.svelte index ec9855d0..3b7de9d3 100644 --- a/src/routes/(app)/settings/profile/+page.svelte +++ b/src/routes/(app)/settings/profile/+page.svelte @@ -116,7 +116,7 @@ > handleFile(e, "picture")} /> @@ -158,7 +158,7 @@ > handleFile(e, "banner")} /> diff --git a/src/routes/(static)/login/+page.svelte b/src/routes/(static)/login/+page.svelte index 1a1f745c..825f6a88 100644 --- a/src/routes/(static)/login/+page.svelte +++ b/src/routes/(static)/login/+page.svelte @@ -124,7 +124,7 @@

{$t("login.signIn")}

{#if form?.error && !form?.message.includes("2fa")} diff --git a/src/routes/(static)/register/+page.svelte b/src/routes/(static)/register/+page.svelte index edb63de1..b0368d47 100644 --- a/src/routes/(static)/register/+page.svelte +++ b/src/routes/(static)/register/+page.svelte @@ -222,7 +222,7 @@

{$t("login.createAccount")}

handleFile(e, "picture")} /> -- cgit v1.2.3 From 9bc38ec4d9f75bcbfa111ed7bf5c9d810b62b86d Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 9 Sep 2025 15:15:24 -0700 Subject: dark mode fix --- src/app.css | 10 +++++++++- src/routes/(app)/settings/nostr/+page.svelte | 3 ++- src/routes/+layout.svelte | 14 +++++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/app.css b/src/app.css index fca47b23..901893b9 100644 --- a/src/app.css +++ b/src/app.css @@ -48,7 +48,15 @@ html { font-family: PublicSans, sans-serif; } .input { @apply input-lg input-primary h-16 px-4; } /* If you want to *turn* native inputs into DaisyUI inputs via @apply: */ -input:not([type="range"]) { @apply input input-lg input-primary h-16 px-4; } +input[type="text"], +input[type="email"], +input[type="password"], +input[type="number"], +input[type="search"], +input[type="url"], +input[type="tel"] { + @apply input input-lg input-primary h-16 px-4; +} textarea { @apply textarea textarea-lg textarea-primary py-4; } select { @apply select select-lg select-primary h-16 px-4; } diff --git a/src/routes/(app)/settings/nostr/+page.svelte b/src/routes/(app)/settings/nostr/+page.svelte index 9129e8f3..7168f4f7 100644 --- a/src/routes/(app)/settings/nostr/+page.svelte +++ b/src/routes/(app)/settings/nostr/+page.svelte @@ -172,7 +172,7 @@ onclick={() => copy(npub)} aria-label="Copy" > {$t("accounts.copy")} +{$t("accounts.qr")}
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 8ae31608..7653e05a 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -7,13 +7,21 @@ import { onMount } from "svelte"; import { installPrompt, theme as themeStore } from "$lib/store"; + +const map = { light: 'lofi', dark: 'black', system: 'system' }; + let { data, children } = $props(); let { pathname, theme } = $state(data); $effect(() => ($themeStore = data.theme)); $effect(() => (theme = $themeStore)); - $effect(() => { - document.documentElement.setAttribute("data-theme", theme); - }); +$effect(() => { + const v = map[theme] ?? theme; + if (!v || v === 'system') { + document.documentElement.removeAttribute('data-theme'); + } else { + document.documentElement.setAttribute('data-theme', v); + } +}); let host = PUBLIC_DOMAIN.includes("localhost") ? `http://${PUBLIC_DOMAIN}` -- cgit v1.2.3 From cb5aecd7af99db4c47ad82e91e70ebad666e96e7 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 9 Sep 2025 15:23:16 -0700 Subject: remove unused import --- .../(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte | 1 - 1 file changed, 1 deletion(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte index 200c93d5..1551cc2c 100644 --- a/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte +++ b/src/routes/(app)/send/bitcoin/[address]/[amount]/[...feeRate]/+page.svelte @@ -13,7 +13,6 @@ import { applyAction } from "$app/forms"; import { hex as hexUtil } from "@scure/base"; import * as btc from "@scure/btc-signer"; - import { hash160 } from "@scure/btc-signer/utils"; import { decrypt } from "nostr-tools/nip49"; import { HDKey } from "@scure/bip32"; import { entropyToMnemonic, mnemonicToSeed } from "@scure/bip39"; -- cgit v1.2.3 From 3083a8675ee2378513d63954ca1bd5468f738660 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 13 Sep 2025 15:11:04 -0500 Subject: Add message decryption Currently, this just takes a message from the text box and returns its decrypted version, but next I will make it decrypt messages it gets from relays. This code works with both NIP-07 (window.nostr) and getPrivateKey(). If both are available, it chooses NIP-07 like the encryption code. --- src/lib/nip17.ts | 11 +++++++++++ src/routes/(app)/dm/[recipient]/+page.svelte | 21 +++++++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/lib/nip17.ts b/src/lib/nip17.ts index 6a9888b6..84790275 100644 --- a/src/lib/nip17.ts +++ b/src/lib/nip17.ts @@ -78,3 +78,14 @@ const giftWrap = (event: object, receiverPK: string) => { return finalizeEvent(giftWrapEvent, secretKey); } + +export const decryptNIP17MessageNIP07 = async (text: string): string => { + const wrapped = JSON.parse(text); + const sealedText = await window.nostr.nip44.decrypt( + wrapped.pubkey, wrapped.content); + const sealed = JSON.parse(sealedText); + const rumourText = await window.nostr.nip44.decrypt( + sealed.pubkey, sealed.content); + const rumour = JSON.parse(rumourText); + return rumour.content; +} diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 446a1068..8e01b7a8 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -1,8 +1,8 @@
@@ -43,5 +55,6 @@ +
-- cgit v1.2.3 From 48d2cac1abf64d549cf10c18c90b875a086c575f Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 27 Sep 2025 21:06:48 -0500 Subject: Upload DMs to specified relays Currently, this is a comma-separated list specified in the env var PUBLIC_DM_RELAYS. Later, it will use users' preferences. --- src/routes/(app)/dm/[recipient]/+page.svelte | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 8e01b7a8..4f02829d 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -2,12 +2,17 @@ import Icon from "$comp/Icon.svelte"; import { hexToBytes, bytesToHex } from '@noble/hashes/utils'; import * as toolsnip17 from 'nostr-tools/nip17'; + import { SimplePool } from 'nostr-tools/pool'; import * as libnip17 from '$lib/nip17'; import { sign, getPrivateKey } from '$lib/nostr'; let { data } = $props(); const { user, recipient } = data; + const pool = new SimplePool(); + import { PUBLIC_DM_RELAYS } from '$env/static/public'; + const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(','); + let text = $state(""); const btnCreateMessage = async () => { @@ -20,8 +25,7 @@ event = libnip17.createNIP17MessageSK(text, sk, recipient.pubkey); } - // TODO send event to relays instead of displaying it - text = JSON.stringify(event); + await Promise.any(pool.publish(DM_RELAYS_LIST, event)); } const btnDecryptMessage = async () => { -- cgit v1.2.3 From 2f2348f459e3b6683284975e63df356e8c821bdc Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Wed, 1 Oct 2025 11:13:09 -0500 Subject: Fetch & display received DMs from relays --- src/lib/nip17.ts | 6 ++---- src/routes/(app)/dm/[recipient]/+page.svelte | 27 +++++++++++++++++++-------- 2 files changed, 21 insertions(+), 12 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/lib/nip17.ts b/src/lib/nip17.ts index 84790275..495f897e 100644 --- a/src/lib/nip17.ts +++ b/src/lib/nip17.ts @@ -79,13 +79,11 @@ const giftWrap = (event: object, receiverPK: string) => { return finalizeEvent(giftWrapEvent, secretKey); } -export const decryptNIP17MessageNIP07 = async (text: string): string => { - const wrapped = JSON.parse(text); +export const decryptNIP17MessageNIP07 = async (wrapped: object): object => { const sealedText = await window.nostr.nip44.decrypt( wrapped.pubkey, wrapped.content); const sealed = JSON.parse(sealedText); const rumourText = await window.nostr.nip44.decrypt( sealed.pubkey, sealed.content); - const rumour = JSON.parse(rumourText); - return rumour.content; + return JSON.parse(rumourText); } diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 4f02829d..971c0cd5 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -12,8 +12,16 @@ const pool = new SimplePool(); import { PUBLIC_DM_RELAYS } from '$env/static/public'; const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(','); + const DM_FETCH_LIMIT = 256; let text = $state(""); + let events = $state([]); + + pool.querySync(DM_RELAYS_LIST, + { kinds: [1059], "#p": [user.pubkey], limit: DM_FETCH_LIMIT }) + .then(wrapped => + Promise.all(wrapped.map(decryptMessage)) + .then(rumours => events = rumours)); const btnCreateMessage = async () => { let event; @@ -28,15 +36,12 @@ await Promise.any(pool.publish(DM_RELAYS_LIST, event)); } - const btnDecryptMessage = async () => { + const decryptMessage = async (event: object): object => { if (window.nostr) { - text = await libnip17.decryptNIP17MessageNIP07(text); + return await libnip17.decryptNIP17MessageNIP07(event); } else { - // TODO get events from relays instead of the text box - const wrapped = JSON.parse(text); const sk = await getPrivateKey(user); - const rumour = toolsnip17.unwrapEvent(wrapped, sk); - text = rumour.content; + return toolsnip17.unwrapEvent(event, sk); } } @@ -56,9 +61,15 @@ >Direct Messages

You are sending nostr messages to {recipient.username}.

+

Received Messages:

+
    + {#each events as evt} +
  • {new Date(evt.created_at * 1000)}: "{evt.content}"
  • + {/each} +
+ - - +
-- cgit v1.2.3 From 878f12ec5fb2b301cd1542b6bdf512e0ad4df39b Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Wed, 1 Oct 2025 11:33:17 -0500 Subject: Only show DMs from sender/receiver You can find DMs from another user by going to their page. --- src/routes/(app)/dm/[recipient]/+page.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 971c0cd5..d450849a 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -64,7 +64,9 @@

Received Messages:

    {#each events as evt} -
  • {new Date(evt.created_at * 1000)}: "{evt.content}"
  • + {#if evt.pubkey === recipient.pubkey || evt.pubkey === user.pubkey} +
  • {new Date(evt.created_at * 1000)}: "{evt.content}"
  • + {/if} {/each}
-- cgit v1.2.3 From 8eaf72778a8f8f84f9499a68cd4a5e798135c0f4 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Wed, 1 Oct 2025 11:41:55 -0500 Subject: Improve displayed message format --- src/routes/(app)/dm/[recipient]/+page.svelte | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index d450849a..0c349f45 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -44,6 +44,16 @@ return toolsnip17.unwrapEvent(event, sk); } } + + const dmName = (pubkey: string): string => { + if (pubkey === user.pubkey) { + return user.username; + } else if (pubkey === recipient.pubkey) { + return recipient.username; + } else { + return pubkey; + } + }
@@ -61,11 +71,11 @@ >Direct Messages

You are sending nostr messages to {recipient.username}.

-

Received Messages:

+

Past Messages:

    {#each events as evt} {#if evt.pubkey === recipient.pubkey || evt.pubkey === user.pubkey} -
  • {new Date(evt.created_at * 1000)}: "{evt.content}"
  • +
  • [{new Date(evt.created_at * 1000).toISOString()}] {dmName(evt.pubkey)}: "{evt.content}"
  • {/if} {/each}
-- cgit v1.2.3 From c6b94b9ac035bf7cc27aa441d1675cb15a1f2fd3 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 2 Oct 2025 08:19:01 -0500 Subject: Show past messages in chronological order --- src/routes/(app)/dm/[recipient]/+page.svelte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 0c349f45..75e55eea 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -21,7 +21,10 @@ { kinds: [1059], "#p": [user.pubkey], limit: DM_FETCH_LIMIT }) .then(wrapped => Promise.all(wrapped.map(decryptMessage)) - .then(rumours => events = rumours)); + .then(rumours => { + events = rumours; + events.sort((ev1, ev2) => ev1.created_at - ev2.created_at); + })); const btnCreateMessage = async () => { let event; -- cgit v1.2.3 From 8f161a474d7dba438289d5dbe93684325560f85f Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 2 Oct 2025 10:01:12 -0500 Subject: Send copy of messages to sender --- src/lib/nip17.ts | 18 ++++++++++++------ src/routes/(app)/dm/[recipient]/+page.svelte | 14 ++++++++++---- 2 files changed, 22 insertions(+), 10 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/lib/nip17.ts b/src/lib/nip17.ts index 495f897e..6cb8f9e3 100644 --- a/src/lib/nip17.ts +++ b/src/lib/nip17.ts @@ -7,17 +7,23 @@ const randomTimestamp = () => Math.floor( Date.now() / 1000 - Math.random() * TWO_DAYS); // Create a NIP-17 message using a provided sender secret key. -export const createNIP17MessageSK = (text: string, senderSK: Uint8Array, receiverPK: string) => { +// If wrapPK is specified and different than receiverPK, +// the message will be gift-wrapped to wrapPK +// but the rumour will be sent to receiverPK. +export const createNIP17MessageSK = (text: string, senderSK: Uint8Array, receiverPK: string, wrapPK: string = null) => { const rumour = createRumour(text, getPublicKey(senderSK), receiverPK); - const sealed = sealRumourSK(rumour, senderSK, receiverPK); - return giftWrap(sealed, receiverPK); + const sealed = sealRumourSK(rumour, senderSK, wrapPK || receiverPK); + return giftWrap(sealed, wrapPK || receiverPK); } // Create a NIP-17 message using NIP-07, requiring no sender secret key. -export const createNIP17MessageNIP07 = async (text: string, senderPK: string, receiverPK: string) => { +// If wrapPK is specified and different than receiverPK, +// the message will be gift-wrapped to wrapPK +// but the rumour will be sent to receiverPK. +export const createNIP17MessageNIP07 = async (text: string, senderPK: string, receiverPK: string, wrapPK: string = null) => { const rumour = createRumour(text, senderPK, receiverPK); - const sealed = await sealRumourNip07(rumour, receiverPK); - return giftWrap(sealed, receiverPK); + const sealed = await sealRumourNip07(rumour, wrapPK || receiverPK); + return giftWrap(sealed, wrapPK || receiverPK); } const createRumour = (text: string, senderPK: string, receiverPK: string) => { diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 75e55eea..a4334ff5 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -27,16 +27,22 @@ })); const btnCreateMessage = async () => { - let event; + let event1, event2; if (window.nostr) { - event = await libnip17.createNIP17MessageNIP07( + event1 = await libnip17.createNIP17MessageNIP07( text, user.pubkey, recipient.pubkey); + event2 = await libnip17.createNIP17MessageNIP07( + text, user.pubkey, recipient.pubkey, user.pubkey); } else { const sk = await getPrivateKey(user); - event = libnip17.createNIP17MessageSK(text, sk, recipient.pubkey); + event1 = libnip17.createNIP17MessageSK(text, sk, recipient.pubkey); + event2 = libnip17.createNIP17MessageSK( + text, sk, recipient.pubkey, user.pubkey); } - await Promise.any(pool.publish(DM_RELAYS_LIST, event)); + const p1 = Promise.any(pool.publish(DM_RELAYS_LIST, event1)); + const p2 = Promise.any(pool.publish(DM_RELAYS_LIST, event2)); + await Promise.all([p1, p2]); } const decryptMessage = async (event: object): object => { -- cgit v1.2.3 From d7c45ac379b317b0270f58e4366b2bec229213f2 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 2 Oct 2025 10:02:13 -0500 Subject: Don't show quotes around past messages This isn't really necessary with the current format - I only added quotes because the messages were right next to ISO 8601 timestamps. --- src/routes/(app)/dm/[recipient]/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index a4334ff5..f41f9a9f 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -84,7 +84,7 @@
    {#each events as evt} {#if evt.pubkey === recipient.pubkey || evt.pubkey === user.pubkey} -
  • [{new Date(evt.created_at * 1000).toISOString()}] {dmName(evt.pubkey)}: "{evt.content}"
  • +
  • [{new Date(evt.created_at * 1000).toISOString()}] {dmName(evt.pubkey)}: {evt.content}
  • {/if} {/each}
-- cgit v1.2.3 From bb168314227f94db7457f541568c3386380acba5 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 2 Oct 2025 10:45:55 -0500 Subject: Refresh messages after sending --- src/routes/(app)/dm/[recipient]/+page.svelte | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index f41f9a9f..9fed1aad 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -17,14 +17,17 @@ let text = $state(""); let events = $state([]); - pool.querySync(DM_RELAYS_LIST, - { kinds: [1059], "#p": [user.pubkey], limit: DM_FETCH_LIMIT }) - .then(wrapped => - Promise.all(wrapped.map(decryptMessage)) - .then(rumours => { - events = rumours; - events.sort((ev1, ev2) => ev1.created_at - ev2.created_at); - })); + const loadEvents = () => { + const filter = { kinds: [1059], "#p": [user.pubkey], limit: DM_FETCH_LIMIT }; + pool.querySync(DM_RELAYS_LIST, filter).then(wrapped => + Promise.all(wrapped.map(decryptMessage)) + .then(rumours => { + events = rumours; + events.sort((ev1, ev2) => ev1.created_at - ev2.created_at); + })); + } + + loadEvents(); const btnCreateMessage = async () => { let event1, event2; @@ -43,6 +46,7 @@ const p1 = Promise.any(pool.publish(DM_RELAYS_LIST, event1)); const p2 = Promise.any(pool.publish(DM_RELAYS_LIST, event2)); await Promise.all([p1, p2]); + loadEvents(); } const decryptMessage = async (event: object): object => { -- cgit v1.2.3 From 2bb7a833e8635edcaa2376b325393858fe451277 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 2 Oct 2025 10:46:07 -0500 Subject: Fix back arrow in sending UI --- src/routes/(app)/dm/[recipient]/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 9fed1aad..7f4952ef 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -73,7 +73,7 @@ -- cgit v1.2.3 From 07e49888b3fb9909b2c998d99bd328eecadce73f Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 2 Oct 2025 11:39:07 -0500 Subject: Check pubkey as part of nip07 check --- src/routes/(app)/dm/[recipient]/+page.svelte | 47 ++++++++++++++++------------ 1 file changed, 27 insertions(+), 20 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 7f4952ef..63957f44 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -15,32 +15,39 @@ const DM_FETCH_LIMIT = 256; let text = $state(""); - let events = $state([]); - - const loadEvents = () => { - const filter = { kinds: [1059], "#p": [user.pubkey], limit: DM_FETCH_LIMIT }; - pool.querySync(DM_RELAYS_LIST, filter).then(wrapped => - Promise.all(wrapped.map(decryptMessage)) - .then(rumours => { - events = rumours; - events.sort((ev1, ev2) => ev1.created_at - ev2.created_at); - })); + let messageRumours = $state([]); + + const loadEvents = async () => { + const wrapped = await pool.querySync( + DM_RELAYS_LIST, + { kinds: [1059], "#p": [user.pubkey], limit: DM_FETCH_LIMIT } + ); + + // intentionally decrypting sequentially to avoid having a bunch of popups + let decryptedRumours = []; + for (event of wrapped) { + const rumour = await decryptMessage(event); + decryptedRumours.push(rumour); + } + decryptedRumours.sort((ev1, ev2) => ev1.created_at - ev2.created_at); + + messageRumours = decryptedRumours; } loadEvents(); - const btnCreateMessage = async () => { + const sendMessage = async (message: string) => { let event1, event2; - if (window.nostr) { + if (await window.nostr.getPublicKey() === user.pubkey) { event1 = await libnip17.createNIP17MessageNIP07( - text, user.pubkey, recipient.pubkey); + message, user.pubkey, recipient.pubkey); event2 = await libnip17.createNIP17MessageNIP07( - text, user.pubkey, recipient.pubkey, user.pubkey); + message, user.pubkey, recipient.pubkey, user.pubkey); } else { const sk = await getPrivateKey(user); - event1 = libnip17.createNIP17MessageSK(text, sk, recipient.pubkey); + event1 = libnip17.createNIP17MessageSK(message, sk, recipient.pubkey); event2 = libnip17.createNIP17MessageSK( - text, sk, recipient.pubkey, user.pubkey); + message, sk, recipient.pubkey, user.pubkey); } const p1 = Promise.any(pool.publish(DM_RELAYS_LIST, event1)); @@ -50,8 +57,8 @@ } const decryptMessage = async (event: object): object => { - if (window.nostr) { - return await libnip17.decryptNIP17MessageNIP07(event); + if (await window.nostr.getPublicKey() === user.pubkey) { + return libnip17.decryptNIP17MessageNIP07(event); } else { const sk = await getPrivateKey(user); return toolsnip17.unwrapEvent(event, sk); @@ -86,7 +93,7 @@

Past Messages:

    - {#each events as evt} + {#each messageRumours as evt} {#if evt.pubkey === recipient.pubkey || evt.pubkey === user.pubkey}
  • [{new Date(evt.created_at * 1000).toISOString()}] {dmName(evt.pubkey)}: {evt.content}
  • {/if} @@ -95,6 +102,6 @@ - + sendMessage(text)}>
-- cgit v1.2.3 From cfc644137ed19f80fe6e6d2d9da53000b93ab7b2 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Fri, 3 Oct 2025 11:48:20 -0500 Subject: Separate past messages by date --- src/routes/(app)/dm/[recipient]/+page.svelte | 50 ++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 11 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 63957f44..b7cb53af 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -16,6 +16,26 @@ let text = $state(""); let messageRumours = $state([]); + let dates = $state([]); + + const appendMultimap = (map: Map, key: string, value: object) => { + if (map.has(key)) { + map.get(key).push(value); + } else { + map.set(key, [value]); + } + } + + const zeroPad = (num: number): string => { + return num < 10 ? '0' + num.toString() : num.toString(); + } + + const timeString = (date: Date): string => { + const hour = zeroPad(date.getHours()); + const minute = zeroPad(date.getMinutes()); + const second = zeroPad(date.getSeconds()); + return `${hour}:${minute}:${second}`; + } const loadEvents = async () => { const wrapped = await pool.querySync( @@ -24,14 +44,20 @@ ); // intentionally decrypting sequentially to avoid having a bunch of popups - let decryptedRumours = []; + let decryptedRumours = new Map(); for (event of wrapped) { const rumour = await decryptMessage(event); - decryptedRumours.push(rumour); + const created = Math.floor(rumour.created_at / 86400); + appendMultimap(decryptedRumours, created, rumour); + } + for (const [date, event] of decryptedRumours) { + event.sort((ev1, ev2) => ev1.created_at - ev2.created_at); } - decryptedRumours.sort((ev1, ev2) => ev1.created_at - ev2.created_at); messageRumours = decryptedRumours; + + dates = Array.from(messageRumours.keys()); + dates.sort((d1, d2) => d1 - d2); } loadEvents(); @@ -91,14 +117,16 @@ >Direct Messages

You are sending nostr messages to {recipient.username}.

-

Past Messages:

-
    - {#each messageRumours as evt} - {#if evt.pubkey === recipient.pubkey || evt.pubkey === user.pubkey} -
  • [{new Date(evt.created_at * 1000).toISOString()}] {dmName(evt.pubkey)}: {evt.content}
  • - {/if} - {/each} -
+ {#each dates as day} +

[{new Date(day * 86400 * 1000).toDateString()}]

+
    + {#each messageRumours.get(day) as rumour} + {#if rumour.pubkey === recipient.pubkey || rumour.pubkey === user.pubkey} +
  • {timeString(new Date(rumour.created_at * 1000))} {dmName(rumour.pubkey)}: {rumour.content}
  • + {/if} + {/each} +
+ {/each} -- cgit v1.2.3 From 1d5fa4114d0590572ab74f56db3c82510c418250 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 11 Oct 2025 16:29:49 -0500 Subject: Add function that finds users' 10050 relays --- src/routes/(app)/dm/[recipient]/+page.svelte | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index b7cb53af..88f6d92c 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -37,6 +37,23 @@ return `${hour}:${minute}:${second}`; } + const getPreferredRelays = async (pubkey: string): string[] => { + const events = await pool.querySync( + DM_RELAYS_LIST, { kinds: [10050], limit: 1, authors: [pubkey] } + ); + + let relays = []; + for (const event of events) { + for (const tag of event.tags) { + if (tag.length >= 2 && tag[0] == "relay") { + relays.push(tag[1]); + } + } + } + + return relays; + } + const loadEvents = async () => { const wrapped = await pool.querySync( DM_RELAYS_LIST, -- cgit v1.2.3 From cbf8818aefafbf389401ed76366a24fc641a9e09 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 11 Oct 2025 17:16:06 -0500 Subject: Use users' preferred relays for DMs --- src/routes/(app)/dm/[recipient]/+page.svelte | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 88f6d92c..2bde110b 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -54,10 +54,9 @@ return relays; } - const loadEvents = async () => { + const loadEvents = async (relays: string[]) => { const wrapped = await pool.querySync( - DM_RELAYS_LIST, - { kinds: [1059], "#p": [user.pubkey], limit: DM_FETCH_LIMIT } + relays, { kinds: [1059], "#p": [user.pubkey], limit: DM_FETCH_LIMIT } ); // intentionally decrypting sequentially to avoid having a bunch of popups @@ -77,7 +76,7 @@ dates.sort((d1, d2) => d1 - d2); } - loadEvents(); + getPreferredRelays(user.pubkey).then(loadEvents); const sendMessage = async (message: string) => { let event1, event2; @@ -93,10 +92,12 @@ message, sk, recipient.pubkey, user.pubkey); } - const p1 = Promise.any(pool.publish(DM_RELAYS_LIST, event1)); - const p2 = Promise.any(pool.publish(DM_RELAYS_LIST, event2)); + const recipientRelays = await getPreferredRelays(recipient.pubkey); + const p1 = Promise.any(pool.publish(recipientRelays, event1)); + const senderRelays = await getPreferredRelays(user.pubkey); + const p2 = Promise.any(pool.publish(senderRelays, event2)); await Promise.all([p1, p2]); - loadEvents(); + getPreferredRelays(user.pubkey).then(loadEvents); } const decryptMessage = async (event: object): object => { -- cgit v1.2.3 From 14e10e3a945117deaabc7bf91a551dbe41712910 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 11 Oct 2025 21:07:41 -0500 Subject: Cache users' preferred relays --- src/routes/(app)/dm/[recipient]/+page.svelte | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 2bde110b..020d095c 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -14,6 +14,8 @@ const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(','); const DM_FETCH_LIMIT = 256; + let relayLists = new Map(); + let text = $state(""); let messageRumours = $state([]); let dates = $state([]); @@ -38,6 +40,9 @@ } const getPreferredRelays = async (pubkey: string): string[] => { + if (relayLists.has(pubkey)) + return relayLists.get(pubkey); + const events = await pool.querySync( DM_RELAYS_LIST, { kinds: [10050], limit: 1, authors: [pubkey] } ); @@ -51,6 +56,9 @@ } } + console.log("found", pubkey, relays); + + relayLists.set(pubkey, relays); return relays; } -- cgit v1.2.3 From 7240e0404bb7f69f76a39584d3494e2d125b7b57 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 18 Oct 2025 10:24:25 -0500 Subject: Forbid DMs if recipient has no DM relays --- src/routes/(app)/dm/[recipient]/+page.svelte | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 020d095c..ecc38bed 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -56,8 +56,6 @@ } } - console.log("found", pubkey, relays); - relayLists.set(pubkey, relays); return relays; } @@ -85,6 +83,13 @@ } getPreferredRelays(user.pubkey).then(loadEvents); + getPreferredRelays(recipient.pubkey).then((relays) => { + let sendButton = document.getElementById("send-message"); + if (!relays || relays.length == 0) { + sendButton.disabled = true; + sendButton.value = `Could not find ${recipient.username}'s DM relays.`; + } + }); const sendMessage = async (message: string) => { let event1, event2; @@ -156,6 +161,6 @@ - sendMessage(text)}> + sendMessage(text)}> -- cgit v1.2.3 From 1f95987dbfefd5c337b32f4b01700f2525f40883 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 18 Oct 2025 10:41:02 -0500 Subject: Add warning if user has no DM relays --- src/routes/(app)/dm/[recipient]/+page.svelte | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index ecc38bed..9bebae92 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -19,6 +19,7 @@ let text = $state(""); let messageRumours = $state([]); let dates = $state([]); + let warning = $state(undefined); const appendMultimap = (map: Map, key: string, value: object) => { if (map.has(key)) { @@ -82,7 +83,13 @@ dates.sort((d1, d2) => d1 - d2); } - getPreferredRelays(user.pubkey).then(loadEvents); + getPreferredRelays(user.pubkey).then((relays) => { + if (!relays || relays.length == 0) { + warning.innerText = "WARNING: You haven't set any preferred relays. You won't be able to receive any messages or see the messages you've sent."; + return; + } + loadEvents(relays); + }); getPreferredRelays(recipient.pubkey).then((relays) => { let sendButton = document.getElementById("send-message"); if (!relays || relays.length == 0) { @@ -133,6 +140,12 @@ } + + -- cgit v1.2.3 From 38a691cd6004adde24c913f69b2bbd01b2541fb9 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 18 Oct 2025 19:12:28 -0500 Subject: Add DM expiration functionality --- src/lib/nip17.ts | 23 ++++++++++++++++------- src/routes/(app)/dm/[recipient]/+page.svelte | 23 +++++++++++++++++++---- 2 files changed, 35 insertions(+), 11 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/lib/nip17.ts b/src/lib/nip17.ts index 6cb8f9e3..9dd59f6f 100644 --- a/src/lib/nip17.ts +++ b/src/lib/nip17.ts @@ -10,20 +10,23 @@ const randomTimestamp = () => Math.floor( // If wrapPK is specified and different than receiverPK, // the message will be gift-wrapped to wrapPK // but the rumour will be sent to receiverPK. -export const createNIP17MessageSK = (text: string, senderSK: Uint8Array, receiverPK: string, wrapPK: string = null) => { +export const createNIP17MessageSK = (text: string, senderSK: Uint8Array, receiverPK: string, wrapPK: string = null, expiryDays: number = null) => { const rumour = createRumour(text, getPublicKey(senderSK), receiverPK); const sealed = sealRumourSK(rumour, senderSK, wrapPK || receiverPK); - return giftWrap(sealed, wrapPK || receiverPK); + return giftWrap(sealed, wrapPK || receiverPK, expiryDays); } // Create a NIP-17 message using NIP-07, requiring no sender secret key. // If wrapPK is specified and different than receiverPK, // the message will be gift-wrapped to wrapPK // but the rumour will be sent to receiverPK. -export const createNIP17MessageNIP07 = async (text: string, senderPK: string, receiverPK: string, wrapPK: string = null) => { +// If expiryDays is specified, the message will expire in that many days, +// plus a random amount from 0 to 2 for security. +// (expiration time = random creation time + expiryDays + 2 days) +export const createNIP17MessageNIP07 = async (text: string, senderPK: string, receiverPK: string, wrapPK: string = null, expiryDays: number = null) => { const rumour = createRumour(text, senderPK, receiverPK); const sealed = await sealRumourNip07(rumour, wrapPK || receiverPK); - return giftWrap(sealed, wrapPK || receiverPK); + return giftWrap(sealed, wrapPK || receiverPK, expiryDays); } const createRumour = (text: string, senderPK: string, receiverPK: string) => { @@ -68,16 +71,22 @@ const sealRumourNip07 = async (rumour: object, receiverPK: string) => { return window.nostr.signEvent(sealEvent); } -const giftWrap = (event: object, receiverPK: string) => { +const giftWrap = (event: object, receiverPK: string, expiryDays: number = null) => { const secretKey = generateSecretKey(); const SKString = bytesToHex(secretKey); const conversationKey = u.getConversationKey(secretKey, receiverPK); const encryptedEvent = encrypt(JSON.stringify(event), conversationKey); + const created = randomTimestamp(); + const tags = [["p", receiverPK]]; + if (expiryDays != null) { + const expiration = created + (expiryDays + 2) * 86400; + tags.push(["expiration", expiration.toString()]); + } const giftWrapEvent = { kind: 1059, // gift wrap - created_at: randomTimestamp(), - tags: [["p", receiverPK]], + created_at: created, + tags: tags, pubkey: getPublicKey(secretKey), content: encryptedEvent } diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 9bebae92..e0702bc3 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -61,6 +61,15 @@ return relays; } + const expired = (event: object) => { + for (const tag of event.tags) { + if (tag[0] == "expiration") { + return (Date.now() / 1000) >= Number(tag[1]); + } + } + return false; + } + const loadEvents = async (relays: string[]) => { const wrapped = await pool.querySync( relays, { kinds: [1059], "#p": [user.pubkey], limit: DM_FETCH_LIMIT } @@ -69,9 +78,11 @@ // intentionally decrypting sequentially to avoid having a bunch of popups let decryptedRumours = new Map(); for (event of wrapped) { - const rumour = await decryptMessage(event); - const created = Math.floor(rumour.created_at / 86400); - appendMultimap(decryptedRumours, created, rumour); + if (!expired(event)) { + const rumour = await decryptMessage(event); + const created = Math.floor(rumour.created_at / 86400); + appendMultimap(decryptedRumours, created, rumour); + } else console.log("relay contains expired event", event.id); } for (const [date, event] of decryptedRumours) { event.sort((ev1, ev2) => ev1.created_at - ev2.created_at); @@ -107,11 +118,15 @@ message, user.pubkey, recipient.pubkey, user.pubkey); } else { const sk = await getPrivateKey(user); - event1 = libnip17.createNIP17MessageSK(message, sk, recipient.pubkey); + event1 = libnip17.createNIP17MessageSK( + message, sk, recipient.pubkey); event2 = libnip17.createNIP17MessageSK( message, sk, recipient.pubkey, user.pubkey); } + console.log(Date.now() / 1000); + console.log(event2.tags); + const recipientRelays = await getPreferredRelays(recipient.pubkey); const p1 = Promise.any(pool.publish(recipientRelays, event1)); const senderRelays = await getPreferredRelays(user.pubkey); -- cgit v1.2.3 From 56e45190fd535f5f6c06914a8b9985e07bd7458c Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 18 Oct 2025 20:53:47 -0500 Subject: Add DM expiry UI --- src/routes/(app)/dm/[recipient]/+page.svelte | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index e0702bc3..4f4d4061 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -20,6 +20,8 @@ let messageRumours = $state([]); let dates = $state([]); let warning = $state(undefined); + let expiryEnabled = $state(false); + let expiryDays = $state(7); const appendMultimap = (map: Map, key: string, value: object) => { if (map.has(key)) { @@ -110,18 +112,19 @@ }); const sendMessage = async (message: string) => { + const expiry = expiryEnabled ? expiryDays : null; let event1, event2; if (await window.nostr.getPublicKey() === user.pubkey) { event1 = await libnip17.createNIP17MessageNIP07( - message, user.pubkey, recipient.pubkey); + message, user.pubkey, recipient.pubkey, recipient.pubkey, expiry); event2 = await libnip17.createNIP17MessageNIP07( - message, user.pubkey, recipient.pubkey, user.pubkey); + message, user.pubkey, recipient.pubkey, user.pubkey, expiry); } else { const sk = await getPrivateKey(user); event1 = libnip17.createNIP17MessageSK( - message, sk, recipient.pubkey); + message, sk, recipient.pubkey, recipient.pubkey, expiry); event2 = libnip17.createNIP17MessageSK( - message, sk, recipient.pubkey, user.pubkey); + message, sk, recipient.pubkey, user.pubkey, expiry); } console.log(Date.now() / 1000); @@ -159,6 +162,19 @@ .warning { color: #ff7f00; } + + .inline { + white-space: nowrap; + } + + .short { + width: 6em; + height: 2em; + } + + .tiny { + width: 2em; + }
@@ -189,6 +205,7 @@ + Enable message expiry in days. sendMessage(text)}>

-- cgit v1.2.3 From 53cb36710837f1cdf62b4bcf860cff3d405ec664 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Fri, 31 Oct 2025 19:07:30 -0500 Subject: Add DM Relays to settings page Currently it uses a separate button, since I need to figure out how to get this to work with the different pages. Next, I'll fit it into the save button. --- src/routes/(app)/settings/nostr/+page.svelte | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/settings/nostr/+page.svelte b/src/routes/(app)/settings/nostr/+page.svelte index 7168f4f7..5ca64588 100644 --- a/src/routes/(app)/settings/nostr/+page.svelte +++ b/src/routes/(app)/settings/nostr/+page.svelte @@ -18,6 +18,9 @@ let extensionAvailable = $derived(browser && window.nostr); let { locale } = $derived(user); + import { SimplePool } from 'nostr-tools/pool'; + import { finalizeEvent } from 'nostr-tools/pure'; + let newNsec = $state(), nsec = $state(), pin = $state(""), @@ -41,6 +44,28 @@ await tick(); $save.click(); }; + + const pool = new SimplePool(); + import { PUBLIC_DM_RELAYS } from '$env/static/public'; + const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(','); + let dmRelaysText = $state(''); + let updateRelays = async () => { + const newRelays = dmRelaysText.split('\n'); + const event = { + kind: 10050, + created_at: Math.floor(Date.now() / 1000), + tags: newRelays.map(r => ["relay", r]), + content: "" + }; + let signed; + if (await window.nostr.getPublicKey() === user.pubkey) { + signed = await window.nostr.signEvent(event); + } else { + const sk = await getPrivateKey(user); + signed = finalizeEvent(event, sk); + } + await Promise.any(pool.publish(DM_RELAYS_LIST, signed)); + } @@ -231,3 +256,11 @@ {/if} {/if} + +

+ Preferred DM Relays +

+Direct Messages meant for you will be published to these relays. + + + -- cgit v1.2.3 From 29c629150aac2aaf089554c75e92f68520d4e1b3 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 1 Nov 2025 11:00:24 -0500 Subject: Use save button to save DM relays This needed me to use document.getElementById instead of Svelte states, but it works. --- src/routes/(app)/settings/+layout.svelte | 28 ++++++++++++++++++++++ src/routes/(app)/settings/nostr/+page.svelte | 36 +++++----------------------- 2 files changed, 34 insertions(+), 30 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/settings/+layout.svelte b/src/routes/(app)/settings/+layout.svelte index 3c2ebd88..75827719 100644 --- a/src/routes/(app)/settings/+layout.svelte +++ b/src/routes/(app)/settings/+layout.svelte @@ -17,6 +17,9 @@ import { getPublicKey } from "nostr-tools"; import { bytesToHex } from "@noble/hashes/utils"; + import { SimplePool } from 'nostr-tools/pool'; + import { finalizeEvent } from 'nostr-tools/pure'; + let { children, data, form } = $props(); let formElement = $state(); @@ -45,7 +48,32 @@ let { about, id, username } = $derived(user); let submitting = $state(); + const pool = new SimplePool(); + import { PUBLIC_DM_RELAYS } from '$env/static/public'; + const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(','); + let updateRelaysIfAvailable = async () => { + const relayEntry = document.getElementById('dmRelays'); + if (!relayEntry) return; + + const newRelays = relayEntry.value.split('\n'); + const event = { + kind: 10050, + created_at: Math.floor(Date.now() / 1000), + tags: newRelays.map(r => ["relay", r]), + content: "" + }; + let signed; + if (await window.nostr.getPublicKey() === user.pubkey) { + signed = await window.nostr.signEvent(event); + } else { + const sk = await getPrivateKey(user); + signed = finalizeEvent(event, sk); + } + await Promise.any(pool.publish(DM_RELAYS_LIST, signed)); + } + async function handleSubmit(e) { + updateRelaysIfAvailable(); e.preventDefault(); try { submitting = true; diff --git a/src/routes/(app)/settings/nostr/+page.svelte b/src/routes/(app)/settings/nostr/+page.svelte index 5ca64588..204b924b 100644 --- a/src/routes/(app)/settings/nostr/+page.svelte +++ b/src/routes/(app)/settings/nostr/+page.svelte @@ -1,4 +1,4 @@ - @@ -257,10 +232,11 @@ {/if} -

+ Preferred DM Relays + +

+ Direct Messages meant for you will be published to these relays.

-Direct Messages meant for you will be published to these relays. - - + -- cgit v1.2.3 From e31a8948e31a44949647f863196b1f68c58161ad Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 1 Nov 2025 11:01:06 -0500 Subject: Load user's DM relays into entry --- src/routes/(app)/settings/nostr/+page.svelte | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/settings/nostr/+page.svelte b/src/routes/(app)/settings/nostr/+page.svelte index 204b924b..e84f10f6 100644 --- a/src/routes/(app)/settings/nostr/+page.svelte +++ b/src/routes/(app)/settings/nostr/+page.svelte @@ -18,6 +18,8 @@ let extensionAvailable = $derived(browser && window.nostr); let { locale } = $derived(user); + import { SimplePool } from 'nostr-tools/pool'; + let newNsec = $state(), nsec = $state(), pin = $state(""), @@ -41,6 +43,30 @@ await tick(); $save.click(); }; + + import { PUBLIC_DM_RELAYS } from '$env/static/public'; + const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(','); + const pool = new SimplePool(); + const getPreferredRelays = async (pubkey: string): string[] => { + const events = await pool.querySync( + DM_RELAYS_LIST, { kinds: [10050], limit: 1, authors: [pubkey] } + ); + + let relays = []; + for (const event of events) { + for (const tag of event.tags) { + if (tag.length >= 2 && tag[0] == "relay") { + relays.push(tag[1]); + } + } + } + + return relays; + } + getPreferredRelays(user.pubkey).then(relays => { + const relayEntry = document.getElementById('dmRelays'); + relayEntry.value = relays.join("\n"); + }); -- cgit v1.2.3 From cb48d8dbd665d4743f9af583307d1fde3423baaa Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 4 Nov 2025 14:46:10 -0500 Subject: Add settings page link to no-relay warning --- src/routes/(app)/dm/[recipient]/+page.svelte | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 4f4d4061..35b5221e 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -19,7 +19,7 @@ let text = $state(""); let messageRumours = $state([]); let dates = $state([]); - let warning = $state(undefined); + let relayWarningShown = $state(false); let expiryEnabled = $state(false); let expiryDays = $state(7); @@ -98,9 +98,10 @@ getPreferredRelays(user.pubkey).then((relays) => { if (!relays || relays.length == 0) { - warning.innerText = "WARNING: You haven't set any preferred relays. You won't be able to receive any messages or see the messages you've sent."; + relayWarningShown = true; return; } + relayWarningShown = false; loadEvents(relays); }); getPreferredRelays(recipient.pubkey).then((relays) => { @@ -163,6 +164,10 @@ color: #ff7f00; } + .link { + text-decoration-line: underline; + } + .inline { white-space: nowrap; } @@ -207,6 +212,8 @@ Enable message expiry in days. sendMessage(text)}> -

+ {#if relayWarningShown} +

WARNING: You haven't set any preferred relays. You won't be able to receive any messages or see the messages you've sent. Go to your Nostr settings to set your relays.

+ {/if} -- cgit v1.2.3 From 8b8c3cb3af361a34417dddca945dea9022fe1ae1 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 4 Nov 2025 14:48:29 -0500 Subject: Clarify how to set multiple DM relays --- src/routes/(app)/settings/nostr/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/settings/nostr/+page.svelte b/src/routes/(app)/settings/nostr/+page.svelte index e84f10f6..fee9885d 100644 --- a/src/routes/(app)/settings/nostr/+page.svelte +++ b/src/routes/(app)/settings/nostr/+page.svelte @@ -262,7 +262,7 @@ Preferred DM Relays

- Direct Messages meant for you will be published to these relays. + Direct Messages meant for you will be published to these relays. To get DMs on multiple relays, put each relay URL on its own line.

-- cgit v1.2.3 From 78667b3f97a1e6c4bd7d09b7ff69602e2e771590 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 6 Nov 2025 16:47:21 -0500 Subject: Add translatability to new DM-related text --- src/locales/en.json | 12 ++++++++++++ src/routes/(app)/dm/[recipient]/+page.svelte | 21 +++++++++------------ src/routes/(app)/settings/nostr/+page.svelte | 4 ++-- 3 files changed, 23 insertions(+), 14 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/locales/en.json b/src/locales/en.json index dd6e29f1..a8b8a06a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -92,6 +92,16 @@ "description": "Over 500 businesses worldwide are accepting bitcoin with Coinos. We hope that yours will be next!" } }, + "dm": { + "header": "Direct Messages", + "recipientMsg": "You are sending nostr messages to [R].", + "sendMessage": "Send Message", + "expiry": "Enable message expiry", + "days": "days", + "relaysNotFound": "Could not find [R]’s DM relays.", + "noRelaysSet": "WARNING: You haven't set any preferred relays. You won't be able to receive any messages or see the messages you've sent.", + "nostrSettingsLink": "Go to your Nostr settings to set your relays." + }, "error": { "header": "Error", "message": "Please try again or contact support.", @@ -337,6 +347,8 @@ "revealNsec": "Reveal secret key", "hideSeed": "Hide seed phrase", "revealSeed": "Reveal seed phrase", + "preferredDM": "Preferred DM Relays", + "preferredDMDescription": "Direct Messages meant for you will be published to these relays. To get DMs on multiple relays, put each relay URL on its own line.", "saveSettings": "Save settings", "bannerImage": "Banner Image", "profileImage": "Profile Image", diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 35b5221e..26b1907d 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -5,6 +5,7 @@ import { SimplePool } from 'nostr-tools/pool'; import * as libnip17 from '$lib/nip17'; import { sign, getPrivateKey } from '$lib/nostr'; + import { t } from "$lib/translations"; let { data } = $props(); const { user, recipient } = data; @@ -22,6 +23,7 @@ let relayWarningShown = $state(false); let expiryEnabled = $state(false); let expiryDays = $state(7); + let canSend = $state(false); const appendMultimap = (map: Map, key: string, value: object) => { if (map.has(key)) { @@ -104,13 +106,8 @@ relayWarningShown = false; loadEvents(relays); }); - getPreferredRelays(recipient.pubkey).then((relays) => { - let sendButton = document.getElementById("send-message"); - if (!relays || relays.length == 0) { - sendButton.disabled = true; - sendButton.value = `Could not find ${recipient.username}'s DM relays.`; - } - }); + getPreferredRelays(recipient.pubkey) + .then((relays) => canSend = relays && relays.length > 0); const sendMessage = async (message: string) => { const expiry = expiryEnabled ? expiryDays : null; @@ -194,8 +191,8 @@

Direct Messages

-

You are sending nostr messages to {recipient.username}.

+ >{$t("dm.header")} +

{$t("dm.recipientMsg").replace("[R]", recipient.username)}

{#each dates as day}

[{new Date(day * 86400 * 1000).toDateString()}]

@@ -210,10 +207,10 @@ - Enable message expiry in days. - sendMessage(text)}> + {$t("dm.expiry")}: {$t("dm.days")}. + sendMessage(text)}> {#if relayWarningShown} -

WARNING: You haven't set any preferred relays. You won't be able to receive any messages or see the messages you've sent. Go to your Nostr settings to set your relays.

+

{$t("dm.noRelaysSet")} {$t("dm.nostrSettingsLink")}

{/if}
diff --git a/src/routes/(app)/settings/nostr/+page.svelte b/src/routes/(app)/settings/nostr/+page.svelte index fee9885d..8557d522 100644 --- a/src/routes/(app)/settings/nostr/+page.svelte +++ b/src/routes/(app)/settings/nostr/+page.svelte @@ -259,10 +259,10 @@ {/if} - Preferred DM Relays + {$t("user.settings.preferredDM")}

- Direct Messages meant for you will be published to these relays. To get DMs on multiple relays, put each relay URL on its own line. + {$t("user.settings.preferredDMDescription")}

-- cgit v1.2.3 From 4461fe5bcfd7f206e273f4fbc582cacade3a8966 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 6 Nov 2025 17:42:21 -0500 Subject: Put DMs in scrollable div --- src/routes/(app)/dm/[recipient]/+page.svelte | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 26b1907d..7f5d36ad 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -194,6 +194,7 @@ >{$t("dm.header")}

{$t("dm.recipientMsg").replace("[R]", recipient.username)}

+
{#each dates as day}

[{new Date(day * 86400 * 1000).toDateString()}]

    @@ -204,6 +205,7 @@ {/each}
{/each} +
-- cgit v1.2.3 From 9b00a8c530314da368a6786997fb69787e2625ba Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Fri, 7 Nov 2025 11:19:12 -0500 Subject: Change relay settings link to anchor --- src/routes/(app)/dm/[recipient]/+page.svelte | 2 +- src/routes/(app)/settings/nostr/+page.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 7f5d36ad..0f232256 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -212,7 +212,7 @@ {$t("dm.expiry")}: {$t("dm.days")}. sendMessage(text)}> {#if relayWarningShown} -

{$t("dm.noRelaysSet")} {$t("dm.nostrSettingsLink")}

+

{$t("dm.noRelaysSet")} {$t("dm.nostrSettingsLink")}

{/if} diff --git a/src/routes/(app)/settings/nostr/+page.svelte b/src/routes/(app)/settings/nostr/+page.svelte index 8557d522..33ff9a59 100644 --- a/src/routes/(app)/settings/nostr/+page.svelte +++ b/src/routes/(app)/settings/nostr/+page.svelte @@ -258,7 +258,7 @@ {/if} - + {$t("user.settings.preferredDM")}

-- cgit v1.2.3 From 6a9ef382bcdc3825b874fbbfbb9d291983365507 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Fri, 7 Nov 2025 11:34:24 -0500 Subject: Scroll messages to bottom when updated --- src/routes/(app)/dm/[recipient]/+page.svelte | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 0f232256..c64df7f7 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -1,4 +1,5 @@ +

The following people have sent messages to you:

    {#each messageSenders as sender} -
  • {sender}
  • +
  • {sender.name}{#if sender.valid} ✓{/if}
  • {/each}

The following people have received messages from you:

    {#each messageRecipients as recipient} -
  • {recipient}
  • +
  • {recipient.name}{#if recipient.valid} ✓{/if}
  • {/each}

-- cgit v1.2.3 From 9616938096f8281204d11a5701e9372c0dfaf6b7 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 13 Dec 2025 19:45:09 -0500 Subject: Improve DM UI appearence --- src/routes/(app)/dm/[recipient]/+page.svelte | 136 ++++++++++++++++++--------- 1 file changed, 89 insertions(+), 47 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/[recipient]/+page.svelte b/src/routes/(app)/dm/[recipient]/+page.svelte index 8014cd2a..787796bd 100644 --- a/src/routes/(app)/dm/[recipient]/+page.svelte +++ b/src/routes/(app)/dm/[recipient]/+page.svelte @@ -1,13 +1,18 @@ -
-
+
+ +

{$t("dm.header")}

-

Welcome to the DM homepage!

- -

The following people have sent messages to you:

-
    - {#each messageSenders as sender} -
  • {sender.name} ({sender.nip05}{#if sender.valid} ✓{/if})
  • - {/each} -
- -

The following people have received messages from you:

-
    - {#each messageRecipients as recipient} -
  • {recipient.name} ({recipient.nip05}{#if recipient.valid} ✓{/if})
  • - {/each} -
+ +

You are talking to {selectedChat}.

+ + {#if selectedChat} + + {/if}
-- cgit v1.2.3 From 09502c9d2843f801a1558278f07b167492e669d7 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Mon, 22 Dec 2025 16:52:11 -0500 Subject: Improve appearance of chat selector --- src/routes/(app)/dm/+page.svelte | 93 ++++++++++++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 19 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/+page.svelte b/src/routes/(app)/dm/+page.svelte index f7215861..f29df543 100644 --- a/src/routes/(app)/dm/+page.svelte +++ b/src/routes/(app)/dm/+page.svelte @@ -4,6 +4,7 @@ import { pTagKeys } from '$lib/nostr'; import { getNostrUserInfo } from '$lib/nip01'; import { getMessageRumours } from '$lib/nip17'; + import { theme } from "$lib/store"; let { data } = $props(); const { user } = data; @@ -12,7 +13,7 @@ const response = await fetch(`/api/users/${pubkey}`); if (response.ok) { const userInfo = await response.json(); - return userInfo.username; + return userInfo.anon ? null : userInfo.username; } else { return null; } @@ -20,9 +21,35 @@ const userInfo = async (pubkey: string) => { const nostrUserInfo = await getNostrUserInfo(pubkey); - const valid = await isValid(pubkey, nostrUserInfo.nip05); + const valid = 'nip05' in nostrUserInfo && await isValid(pubkey, nostrUserInfo.nip05); const username = await usernameFromPubkey(pubkey); - return { name: username || nostrUserInfo.name, nip05: nostrUserInfo.nip05, valid, pubkey }; + return { + name: username || nostrUserInfo?.name, + nip05: nostrUserInfo ? nostrUserInfo.nip05 : null, + valid, pubkey }; + } + + const rumourInvolves = (rumour: object, pubkey: string) => { + if (rumour.pubkey === pubkey) { + return true; + } else { + for (const tagPK of pTagKeys(rumour)) { + if (tagPK === pubkey) { + return true; + } + } + return false; + } + } + + const mostRecentCommunication = (rumours: object[], pubkey: string) => { + let mostRecentTime = null; + for (const rumour of rumours.filter((r) => rumourInvolves(r, pubkey))) { + if (!mostRecentTime || rumour.created_at > mostRecentTime) { + mostRecentTime = rumour.created_at; + } + } + return mostRecentTime; } const updateSendersRecipients = async (rumours: object[]) => { @@ -48,11 +75,13 @@ chatMap.set(recipient.pubkey, recipient); } chats = Array.from(chatMap.values()); + chats.sort((a, b) => mostRecentCommunication(rumours, b.pubkey) - mostRecentCommunication(rumours, a.pubkey)); }; let messageSenders = $state([]); let messageRecipients = $state([]); let chats = $state([]); + $inspect(chats); getMessageRumours(user).then(updateSendersRecipients); let selectedChat = $state(null); @@ -69,18 +98,50 @@ .sidebar { max-width: 300px; - margin: 0px 10px; + padding: 0px 10px 5px; + margin: 0px 5px; + height: 100%; + border-radius: 10px; + } + + .light-sidebar { + background-color: #eeeeee; + } + + .dark-sidebar { + background-color: #222222; } .chat-btn { - min-height: 100px; - border: 1px solid; - padding: 10px; + min-height: 3em; + margin: 2px 0px; + width: 100%; + border-radius: 10px; + } + + .chat-header { + margin: 1em 15px 0.5em; + } + + .light-chat-btn:hover { + background-color: #dddddd; + } + + .dark-chat-btn:hover { + background-color: #333333; + } + + .light-selected { + background-color: #dddddd; + } + + .dark-selected { + background-color: #333333; } .main { padding-left: 0px; - margin-left: auto; + margin-left: 30px; } .secondary { @@ -89,10 +150,11 @@
-
-- cgit v1.2.3 From dc5e28b08b701de05139476c001b25c500a505ce Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 23 Dec 2025 17:19:05 -0500 Subject: Make selectedChat the whole object --- src/routes/(app)/dm/+page.svelte | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/+page.svelte b/src/routes/(app)/dm/+page.svelte index f29df543..280b63d3 100644 --- a/src/routes/(app)/dm/+page.svelte +++ b/src/routes/(app)/dm/+page.svelte @@ -153,9 +153,9 @@

Chats

{#each chats as c} - + {/each}
@@ -163,6 +163,8 @@ class="text-5xl font-medium text-left w-full mx-auto lg:mx-0 md:w-[500px]" >{$t("dm.header")} -

You are talking to {selectedChat}.

+ {#if selectedChat} +

You are talking to {selectedChat.name}.

+ {/if}
-- cgit v1.2.3 From 80b8eb62fb9d0fdbc63e0f71b8516d3c2c9a2b45 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 23 Dec 2025 19:55:53 -0500 Subject: Add individual DMs to DM page --- src/routes/(app)/dm/+page.svelte | 201 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 195 insertions(+), 6 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/+page.svelte b/src/routes/(app)/dm/+page.svelte index 280b63d3..cffc611e 100644 --- a/src/routes/(app)/dm/+page.svelte +++ b/src/routes/(app)/dm/+page.svelte @@ -1,14 +1,22 @@ @@ -153,7 +312,7 @@

Chats

{#each chats as c} - {/each} @@ -164,7 +323,37 @@ >{$t("dm.header")} {#if selectedChat} -

You are talking to {selectedChat.name}.

+

{selectedChat.username || nostrUserInfo.name} + {nostrUserInfo.nip05 || selectedChat.pubkey.substring(0, 16)} +

+ +
+ {#each dates as day} +

{formatDate(new Date(day * 86400 * 1000))}

+
    + {#each messageRumours.get(day) as rumour} + {#if rumour.pubkey === user.pubkey && pTagKeys(rumour).includes(selectedChat.pubkey)} +
  • {rumour.content} {TIME.format(new Date(rumour.created_at * 1000))}
  • + {:else if rumour.pubkey === selectedChat.pubkey && pTagKeys(rumour).includes(user.pubkey)} +
  • {rumour.content} {TIME.format(new Date(rumour.created_at * 1000))}
  • + {/if} + {/each} +
+ {/each} +
+ + + sendMessage(text)}> + {#if relayWarningShown} +

{$t("dm.noRelaysSet")} {$t("dm.nostrSettingsLink")}

+ {/if} + + + : . +

{$t("dm.fadedWarning")}

+ {#if canSend && !canSendExpiring} +

{$t("dm.cantSendExpiring").replace("[R]", selectedChat.username)}

+ {/if} {/if}
-- cgit v1.2.3 From d98b2eb7095e173d5ac417946e5e374598edeced Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Mon, 5 Jan 2026 11:21:11 -0500 Subject: Reload rumours when updateEvents called --- src/routes/(app)/dm/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/+page.svelte b/src/routes/(app)/dm/+page.svelte index cffc611e..54d0bd76 100644 --- a/src/routes/(app)/dm/+page.svelte +++ b/src/routes/(app)/dm/+page.svelte @@ -137,8 +137,8 @@ return eventsMap; } - libnip17.getMessageRumours(user).then((rumours) => allRumours = rumours); const updateEvents = async () => { + allRumours = await libnip17.getMessageRumours(user); const rumours = allRumours.filter((r) => (r.pubkey === user.pubkey && pTagKeys(r).includes(selectedChat.pubkey)) || (r.pubkey === selectedChat.pubkey && pTagKeys(r).includes(user.pubkey))); -- cgit v1.2.3 From 0b4a1ac2d006204e8e79c157e083b21b6fcaedab Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Mon, 5 Jan 2026 11:22:11 -0500 Subject: Allow creation of new chats from dm page --- src/routes/(app)/dm/+page.svelte | 96 +++++++++++++++++++++++++++++++++------- 1 file changed, 81 insertions(+), 15 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/+page.svelte b/src/routes/(app)/dm/+page.svelte index 54d0bd76..c172e604 100644 --- a/src/routes/(app)/dm/+page.svelte +++ b/src/routes/(app)/dm/+page.svelte @@ -2,13 +2,14 @@ import { tick } from "svelte"; import Icon from "$comp/Icon.svelte"; - import { isValid } from "nostr-tools/nip05"; + import { isValid, queryProfile } from "nostr-tools/nip05"; import * as toolsnip17 from 'nostr-tools/nip17'; import { getNostrUserInfo } from '$lib/nip01'; import { relaysSupporting } from '$lib/nip11'; import { getMessageRumours } from '$lib/nip17'; import * as libnip17 from '$lib/nip17'; + import { decode } from 'nostr-tools/nip19'; import { sign, getPrivateKey, pTagKeys } from '$lib/nostr'; import { t } from "$lib/translations"; import { theme } from "$lib/store"; @@ -28,7 +29,11 @@ } const userInfo = async (pubkey: string) => { - const nostrUserInfo = await getNostrUserInfo(pubkey); + if (pubkey.slice(0, 4) === "npub") { + return userInfo(decode(pubkey).data); + } + + const nostrUserInfo = await getNostrUserInfo(pubkey) || {}; const valid = 'nip05' in nostrUserInfo && await isValid(pubkey, nostrUserInfo.nip05); const username = await usernameFromPubkey(pubkey); return { @@ -89,10 +94,11 @@ let messageSenders = $state([]); let messageRecipients = $state([]); let chats = $state([]); - $inspect(chats); getMessageRumours(user).then(updateSendersRecipients); let selectedChat = $state(null); + let creatingNewChat = $state(false); + let searchQuery = $state(""); /// individual chat let text = $state(""); @@ -108,6 +114,9 @@ const selectChat = (c: object) => { selectedChat = c; + if (!chats.includes(c)) { + chats.unshift(c); + } userInfo(selectedChat.pubkey).then(info => nostrUserInfo = info); libnip17.getPreferredRelays(selectedChat.pubkey).then((relays) => { canSend = relays && relays.length > 0; @@ -117,6 +126,30 @@ updateEvents(); } + const newChatUsername = async (username: string) => { + const response = await fetch(`/api/users/${username}`); + if (!response.ok) { + alert("Invalid username"); + return; + } + const coinosUserInfo = await response.json(); + newChatPubkey(coinosUserInfo.pubkey); + } + + const newChatNip05 = async (nip05: string) => { + const response = await queryProfile(nip05); + if (response == null) { + alert("Invalid nip-05"); + return; + } + newChatPubkey(response.pubkey); + } + + const newChatPubkey = async (pubkey: string) => { + selectChat(await userInfo(pubkey)); + creatingNewChat = false; + } + const appendMultimap = (map: Map, key: string, value: object) => { if (map.has(key)) { map.get(key).push(value); @@ -206,8 +239,12 @@ } .short { - width: 6em; height: 1.5em; + margin-bottom: 10px; + } + + .small-width { + width: 6em; } .tiny { @@ -239,10 +276,20 @@ } .chat-btn { - min-height: 3em; margin: 2px 0px; - width: 100%; + padding: 0 25px; border-radius: 10px; + min-width: 30px; + text-align: left; + } + + .tall-btn { + min-height: 3em; + width: 100%; + } + + .push-right { + margin-left: auto; } .chat-header { @@ -310,12 +357,31 @@
-

Chats

- {#each chats as c} - + {#if creatingNewChat} + + {/if} + {#if !creatingNewChat} + {#each chats as c} + + {/each} + {:else if searchQuery !== ""} + - {/each} + {#if searchQuery.includes(".")} + + {/if} + {#if searchQuery.length === 64 || searchQuery.slice(0, 4) === "npub"} + + {/if} + {/if}

{$t("dm.header")}

{#if selectedChat} -

{selectedChat.username || nostrUserInfo.name} +

{selectedChat.username || nostrUserInfo.name || "Anonymous"} {nostrUserInfo.nip05 || selectedChat.pubkey.substring(0, 16)}

@@ -343,16 +409,16 @@
- sendMessage(text)}> + sendMessage(text)}> {#if relayWarningShown}

{$t("dm.noRelaysSet")} {$t("dm.nostrSettingsLink")}

{/if} - : . + : .

{$t("dm.fadedWarning")}

{#if canSend && !canSendExpiring} -

{$t("dm.cantSendExpiring").replace("[R]", selectedChat.username)}

+

{$t("dm.cantSendExpiring").replace("[R]", selectedChat.username || selectedChat.name)}

{/if} {/if}
-- cgit v1.2.3 From ff442a41f26b37832a67df760497bd7da742eb45 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Mon, 5 Jan 2026 19:38:24 -0500 Subject: Use consistent names for chats --- src/routes/(app)/dm/+page.svelte | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/routes/(app)/dm/+page.svelte b/src/routes/(app)/dm/+page.svelte index c172e604..171f9a62 100644 --- a/src/routes/(app)/dm/+page.svelte +++ b/src/routes/(app)/dm/+page.svelte @@ -9,7 +9,7 @@ import { relaysSupporting } from '$lib/nip11'; import { getMessageRumours } from '$lib/nip17'; import * as libnip17 from '$lib/nip17'; - import { decode } from 'nostr-tools/nip19'; + import { npubEncode, decode } from 'nostr-tools/nip19'; import { sign, getPrivateKey, pTagKeys } from '$lib/nostr'; import { t } from "$lib/translations"; import { theme } from "$lib/store"; @@ -37,9 +37,26 @@ const valid = 'nip05' in nostrUserInfo && await isValid(pubkey, nostrUserInfo.nip05); const username = await usernameFromPubkey(pubkey); return { - name: username || nostrUserInfo?.name, - nip05: nostrUserInfo ? nostrUserInfo.nip05 : null, - valid, pubkey }; + coinosUsername: username, + nostrName: nostrUserInfo?.name, + nip05: nostrUserInfo?.nip05, + nip05Valid: valid, pubkey }; + } + + const name = (userInfo: object): string => { + return userInfo.coinosUsername || userInfo.nostrName || "Anonymous"; + } + + const id = (userInfo: object): string => { + if (userInfo.nip05 && userInfo.nip05.slice(0, 2) == "_@") { + return userInfo.nip05.slice(2); + } + + return userInfo.nip05 || (npubEncode(userInfo.pubkey).slice(0, 16) + "..."); + } + + const idValid = (userInfo: object): string => { + return !userInfo.nip05 || userInfo.nip05Valid; } const rumourInvolves = (rumour: object, pubkey: string) => { @@ -364,7 +381,7 @@ {#if !creatingNewChat} {#each chats as c} {/each} {:else if searchQuery !== ""} @@ -389,8 +406,8 @@ >{$t("dm.header")} {#if selectedChat} -

{selectedChat.username || nostrUserInfo.name || "Anonymous"} - {nostrUserInfo.nip05 || selectedChat.pubkey.substring(0, 16)} +

{name(selectedChat)} + {id(selectedChat)}

@@ -409,7 +426,7 @@
- sendMessage(text)}> + sendMessage(text)}> {#if relayWarningShown}

{$t("dm.noRelaysSet")} {$t("dm.nostrSettingsLink")}

{/if} @@ -418,7 +435,7 @@ : .

{$t("dm.fadedWarning")}

{#if canSend && !canSendExpiring} -

{$t("dm.cantSendExpiring").replace("[R]", selectedChat.username || selectedChat.name)}

+

{name(selectedChat)}

{/if} {/if}
-- cgit v1.2.3 From 8b2179ca8cbda0dca18dd5a52a8b94514ba6160b Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 6 Jan 2026 11:33:22 -0500 Subject: Add links to shared dm page --- src/components/Menu.svelte | 1 + src/locales/en.json | 1 + src/locales/fr.json | 1 + .../(app)/[username]/(profile)/+layout.svelte | 2 +- src/routes/(app)/dm/+page.svelte | 36 ++++++++++++++++------ 5 files changed, 30 insertions(+), 11 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/components/Menu.svelte b/src/components/Menu.svelte index e433fefb..49e477a1 100644 --- a/src/components/Menu.svelte +++ b/src/components/Menu.svelte @@ -16,6 +16,7 @@ // icon: "ph:storefront-bold", // href: `https://coinosmerch.com`, // }, + { key: "nav.dm", icon: "ph:chat-bold", href: `/dm` }, { key: "nav.dark", icon: "ph:moon-stars-bold", href: `/dark` }, { key: "nav.signOut", icon: "ph:sign-out-bold", href: `/logout` }, ]; diff --git a/src/locales/en.json b/src/locales/en.json index 03579132..670fa97b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -227,6 +227,7 @@ "light": "Light", "about": "About", "docs": "Docs", + "dm": "Nostr Messages", "messages": "Messages", "funds": "Funds", "map": "Map", diff --git a/src/locales/fr.json b/src/locales/fr.json index c891293d..da56df68 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -150,6 +150,7 @@ "merch": "Marchandise", "about": "À Propos", "docs": "Documents", + "dm": "Messages Nostr", "messages": "Messages", "funds": "Fonds", "dark": "Sombre", diff --git a/src/routes/(app)/[username]/(profile)/+layout.svelte b/src/routes/(app)/[username]/(profile)/+layout.svelte index 8ce4150c..515dba5d 100644 --- a/src/routes/(app)/[username]/(profile)/+layout.svelte +++ b/src/routes/(app)/[username]/(profile)/+layout.svelte @@ -142,7 +142,7 @@ {:else} diff --git a/src/routes/(app)/dm/+page.svelte b/src/routes/(app)/dm/+page.svelte index 171f9a62..7578c27c 100644 --- a/src/routes/(app)/dm/+page.svelte +++ b/src/routes/(app)/dm/+page.svelte @@ -1,5 +1,5 @@ - - - -
-
-

{recipient.username || nostrUserInfo.name} - {#if nostrUserInfo.nip05} - {nostrUserInfo.nip05} - {/if} -

- -
- {#each dates as day} -

{formatDate(new Date(day * 86400 * 1000))}

-
    - {#each messageRumours.get(day) as rumour} - {#if rumour.pubkey === user.pubkey} -
  • {rumour.content} {TIME.format(new Date(rumour.created_at * 1000))}
  • - {:else if rumour.pubkey === recipient.pubkey} -
  • {rumour.content} {TIME.format(new Date(rumour.created_at * 1000))}
  • - {/if} - {/each} -
- {/each} -
- - - sendMessage(text)}> - {#if relayWarningShown} -

{$t("dm.noRelaysSet")} {$t("dm.nostrSettingsLink")}

- {/if} - - - : . -

{$t("dm.fadedWarning")}

- {#if canSend && !canSendExpiring} -

{$t("dm.cantSendExpiring").replace("[R]", recipient.username)}

- {/if} -
-
-- cgit v1.2.3 From 4bc94247c382898c0e0d76439fe0bdce68755d69 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 6 Jan 2026 15:39:04 -0500 Subject: Add translations for new text --- src/locales/en.json | 6 ++++++ src/locales/fr.json | 6 ++++++ src/routes/(app)/dm/+page.svelte | 13 ++++++++----- 3 files changed, 20 insertions(+), 5 deletions(-) (limited to 'src/routes/(app)') diff --git a/src/locales/en.json b/src/locales/en.json index 670fa97b..e7b8a90f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -93,7 +93,13 @@ } }, "dm": { + "chatHeader": "Chats", + "newChatHeader": "New Chat", "header": "Direct Messages", + "searchPrompt": "username, nip-05 or pubkey", + "findByUsername": "Find by username", + "findByNIP05": "Find by NIP-05", + "findByPubkey": "Find by pubkey", "recipientMsg": "You are sending nostr messages to [R].", "sendMessage": "Send Message", "expiry": "Enable message expiry", diff --git a/src/locales/fr.json b/src/locales/fr.json index da56df68..d3b46c91 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -25,7 +25,13 @@ } }, "dm": { + "chatHeader": "Discussions", + "newChatHeader": "Nouveau Discussion", "header": "Messages directs", + "searchPrompt": "nom, nip-05 ou clé", + "findByUsername": "Trouvez par nom d'utilisateur", + "findByNIP05": "Trouvez par NIP-05", + "findByPubkey": "Trouvez par clé", "recipientMsg": "Vous envoyez messages nostr à [R].", "sendMessage": "Envoyez Message", "expiry": "Expirez messages", diff --git a/src/routes/(app)/dm/+page.svelte b/src/routes/(app)/dm/+page.svelte index 7578c27c..32b39def 100644 --- a/src/routes/(app)/dm/+page.svelte +++ b/src/routes/(app)/dm/+page.svelte @@ -385,9 +385,12 @@
-

{creatingNewChat ? "New Chat" : "Chats"}

+

+ {creatingNewChat ? $t("dm.newChatHeader") : $t("dm.chatHeader")} + +

{#if creatingNewChat} - + {/if} {#if !creatingNewChat} {#each chats as c} @@ -397,16 +400,16 @@ {/each} {:else if searchQuery !== ""} {#if searchQuery.includes(".")} {/if} {#if searchQuery.length === 64 || searchQuery.slice(0, 4) === "npub"} {/if} {/if} -- cgit v1.2.3 From 9175c76d7f03c0c62323202fdab3e4ab35854855 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Fri, 9 Jan 2026 09:00:32 -0500 Subject: Hide muted chats --- src/lib/nip51.ts | 47 ++++++++++++++++++++++++++++++++++++++++ src/locales/en.json | 2 ++ src/routes/(app)/dm/+page.svelte | 24 ++++++++++++++++---- 3 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 src/lib/nip51.ts (limited to 'src/routes/(app)') diff --git a/src/lib/nip51.ts b/src/lib/nip51.ts new file mode 100644 index 00000000..142f0bbf --- /dev/null +++ b/src/lib/nip51.ts @@ -0,0 +1,47 @@ +import { SimplePool } from 'nostr-tools/pool'; +const pool = new SimplePool(); + +import { PUBLIC_DM_RELAYS } from '$env/static/public'; +const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(','); + +// export const mute = async (pubkey: string, hidden: bool) => { + +// } + +// export const unmute = async (pubkey: string) => { + +// } + +export const mutedAccounts = async (user: object): Set => { + const { shown, hidden } = await getMuteLists(user); + let muted = new Set(); + for (const pubkey of shown) { + muted.add(pubkey); + } + for (const pubkey of hidden) { + muted.add(pubkey); + } + return muted; +} + +const getMuteLists = async (user: object): object => { + const listEvents = await pool.querySync( + DM_RELAYS_LIST, { kinds: [10000], "authors": [user.pubkey], limit: 1 } + ); + if (listEvents.length == 0) { + return { shown: [], hidden: [] }; + } + + let shown = []; + for (const tag of listEvents[0].tags) { + if (tag.length >= 2 && tag[0] == "p") { + shown.push(tag[1]); + } + } + + return { shown, hidden: [] }; +} + +// const publishMuteList = async (shown: Array, hidden: Array) => { + +// } diff --git a/src/locales/en.json b/src/locales/en.json index e7b8a90f..16e60d1f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -102,6 +102,8 @@ "findByPubkey": "Find by pubkey", "recipientMsg": "You are sending nostr messages to [R].", "sendMessage": "Send Message", + "mute": "Mute", + "unmute": "Unmute", "expiry": "Enable message expiry", "cantSendExpiring": "None of [R]'s DM relays support message expiry. You can't send expiring messages to them.", "fadedWarning": "To avoid metadata leaks, messages expire at a random time up to 2 days after you specify; each message has its own random expiry time. Messages which appear faded are close to expiring; nearby messages may be missing.", diff --git a/src/routes/(app)/dm/+page.svelte b/src/routes/(app)/dm/+page.svelte index 32b39def..336733e0 100644 --- a/src/routes/(app)/dm/+page.svelte +++ b/src/routes/(app)/dm/+page.svelte @@ -10,6 +10,7 @@ import { getMessageRumours } from '$lib/nip17'; import * as libnip17 from '$lib/nip17'; import { npubEncode, decode } from 'nostr-tools/nip19'; + import { mutedAccounts } from '$lib/nip51'; import { sign, getPrivateKey, pTagKeys } from '$lib/nostr'; import { t } from "$lib/translations"; import { theme } from "$lib/store"; @@ -17,6 +18,9 @@ let { data } = $props(); const { user } = data; + let muted = $state(new Set()); + mutedAccounts(user).then((m) => muted = m); + /// chat selection const usernameFromPubkey = async (pubkey: string): string => { const response = await fetch(`/api/users/${pubkey}`); @@ -129,9 +133,18 @@ let canSendExpiring = $state(false); let nostrUserInfo = $state({}); + const includesPubkey = (chats: object[], pubkey: string) => { + for (const c of chats) { + if (c.pubkey === pubkey) { + return true; + } + } + return false; + } + const selectChat = (c: object) => { selectedChat = c; - if (!chats.includes(c)) { + if (!includesPubkey(chats, c.pubkey)) { chats.unshift(c); } userInfo(selectedChat.pubkey).then(info => nostrUserInfo = info); @@ -394,9 +407,11 @@ {/if} {#if !creatingNewChat} {#each chats as c} - + {#if (selectedChat && selectedChat.pubkey === c.pubkey) || !(muted && muted.has(c.pubkey))} + + {/if} {/each} {:else if searchQuery !== ""}