From 148699a3e7cf1850b35db4c1b2e94d10ec9ec3fd Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Fri, 13 Feb 2026 15:12:06 -0500 Subject: Localize chat addition error messages --- src/locales/en.json | 2 ++ src/locales/fr.json | 2 ++ src/routes/(app)/dm/+page.svelte | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/locales/en.json b/src/locales/en.json index ae25ccc8..948ed4c8 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -109,6 +109,8 @@ "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.", + "usernameNotFound": "An account with that username was not found.", + "nip05NotFound": "A Nostr account with that NIP-05 was not found.", "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.", diff --git a/src/locales/fr.json b/src/locales/fr.json index 9b2c767b..2acfb417 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -41,6 +41,8 @@ "expiry": "Expirez messages", "cantSendExpiring": "Aucun des relais de [R] expirent les messages expirants. Vous ne pouvez pas leur envoyer des messages expirants.", "fadedWarning": "Pour protéger vos métadonées, vos messages expirent à un temps randomée au maximum 2 jours après la date que vous spécifiez; chaque message a son propre temps d’expiration randomée. Les messages qui semble faibles sont proche à expirer; les autres messages autours pouvait avoir déjà disparu.", + "usernameNotFound": "Aucun conte avec ce nom d'utilisateur a été trouvé.", + "nip05NotFound": "Aucun conte Nostr avec ce NIP-05 a été trouvé.", "days": "jours", "relaysNotFound": "Ne pouvait pas trouvez les relais MD de [R].", "noRelaysSet": "AVERTISSEMENT: Vous n’avez pas specifié vos relais préferés pour messages directs. Vous ne pouvez pas reçevoir des messages ou voir les messages que vous avez envoyé.", diff --git a/src/routes/(app)/dm/+page.svelte b/src/routes/(app)/dm/+page.svelte index 80180209..3247cafe 100644 --- a/src/routes/(app)/dm/+page.svelte +++ b/src/routes/(app)/dm/+page.svelte @@ -160,7 +160,7 @@ const selectChatUsername = async (username: string) => { const response = await fetch(`/api/users/${username}`); if (!response.ok) { - alert("Invalid username"); + alert($t("dm.usernameNotFound")); return; } const coinosUserInfo = await response.json(); @@ -170,7 +170,7 @@ const selectChatNip05 = async (nip05: string) => { const response = await queryProfile(nip05); if (response == null) { - alert("Invalid nip-05"); + alert($t("dm.nip05NotFound")); return; } selectChatPubkey(response.pubkey); -- cgit v1.2.3 From 3472f3fb2bb1ca5bbcf03abc181aee6c04ab2acd Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 14 Feb 2026 09:57:08 -0500 Subject: Increase DM fetch limit to 2^16 Given that this is for all recipients combined, 2^16 is much more reasonable than 2^8. --- src/lib/nip17.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/lib/nip17.ts b/src/lib/nip17.ts index 6f29940d..9a6d01f6 100644 --- a/src/lib/nip17.ts +++ b/src/lib/nip17.ts @@ -19,7 +19,7 @@ let relayListCache = new Map(); import { PUBLIC_DM_RELAYS } from '$env/static/public'; const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(','); -const DM_FETCH_LIMIT = 256; +const DM_FETCH_LIMIT = 65536; // Ensures we have access to a secret key, triggering the signer modal if needed. // Returns the sk as Uint8Array, or null if using NIP-07 extension. -- cgit v1.2.3 From 45fcf3586e5e553a71fe5395da051e424069bd4c Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 14 Feb 2026 11:03:08 -0500 Subject: Hide empty chats when unselected The only way these chats appear is if you add them, so if you leave them without sending a message, they are removed from the list. You can always add them again using the same way you got them. --- src/routes/(app)/dm/+page.svelte | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/routes/(app)/dm/+page.svelte b/src/routes/(app)/dm/+page.svelte index 3247cafe..71dd9a9d 100644 --- a/src/routes/(app)/dm/+page.svelte +++ b/src/routes/(app)/dm/+page.svelte @@ -134,6 +134,16 @@ let canSendExpiring = $state(false); let nostrUserInfo = $state({}); + const selectedChatEmpty = () => !messageRumours || messageRumours.size == 0; + + const removeChat = (pubkey: string) => { + for (let i = 0; i < chats.length; i++) { + if (chats[i].pubkey === pubkey) { + chats.splice(i, 1); + } + } + } + const includesPubkey = (chats: object[], pubkey: string) => { for (const c of chats) { if (c.pubkey === pubkey) { @@ -144,6 +154,10 @@ } const selectChat = (c: object) => { + if (selectedChat && selectedChatEmpty()) { + removeChat(selectedChat.pubkey); + } + selectedChat = c; if (!includesPubkey(chats, c.pubkey)) { chats.unshift(c); @@ -427,7 +441,7 @@

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

{#if creatingNewChat} -- cgit v1.2.3 From 5b81820f2d3d1d278e02c7858f17c8f69232d8ec Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 14 Feb 2026 11:17:44 -0500 Subject: Create only one event when sending DM to self Normally, the system creates two identical events: one encrypted to the sender and one to the recipient. If those are the same (i.e. you send a message to yourself), this creates two identical messages. The gift-wraps already give the two different events different pubkeys, created_at times, IDs and tags, so there should be no way to associate them with each other. Therefore, this change should not allow attackers to see that a message is sent to oneself by looking for a matching one. --- src/lib/nip17.ts | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/lib/nip17.ts b/src/lib/nip17.ts index 9a6d01f6..b8352d1e 100644 --- a/src/lib/nip17.ts +++ b/src/lib/nip17.ts @@ -313,6 +313,19 @@ const publishToPreferred = async (event: object, pubkey: string, expiryEnabled: } } +const createAndPublishMessageEvent = async (sk: Uint8Array, message: string, userPK: string, recipientPK: string, eventFor: string, expiryDays?: number) => { + let event; + if (sk === null) { // Use NIP-07 extension + event = await createNIP17MessageNIP07( + message, userPK, recipientPK, eventFor, expiryDays); + } else { + event1 = createNIP17MessageSK( + message, sk, recipientPK, eventFor, expiryDays); + } + + return publishToPreferred(event, eventFor, expiryDays != null); +} + // Sends a NIP-17 message from `user` to `recipient`. // If expiryDays is set, messages will expire after that many days. export const send = async (message: string, user: object, recipient: object, expiryDays?: number) => { @@ -322,20 +335,14 @@ export const send = async (message: string, user: object, recipient: object, exp // Store sk for auth handler currentSk = sk; - if (sk === null) { - // Use NIP-07 extension - event1 = await createNIP17MessageNIP07( - message, user.pubkey, recipient.pubkey, recipient.pubkey, expiryDays); - event2 = await createNIP17MessageNIP07( - message, user.pubkey, recipient.pubkey, user.pubkey, expiryDays); + if (user.pubkey === recipient.pubkey) { + return createAndPublishMessageEvent( + sk, message, user.pubkey, recipient.pubkey, user.pubkey, expiryDays); } else { - event1 = createNIP17MessageSK( - message, sk, recipient.pubkey, recipient.pubkey, expiryDays); - event2 = createNIP17MessageSK( - message, sk, recipient.pubkey, user.pubkey, expiryDays); + const p1 = createAndPublishMessageEvent( + sk, message, user.pubkey, recipient.pubkey, recipient.pubkey, expiryDays); + const p2 = createAndPublishMessageEvent( + sk, message, user.pubkey, recipient.pubkey, user.pubkey, expiryDays); + return Promise.all([p1, p2]); } - - const p1 = publishToPreferred(event1, recipient.pubkey, expiryDays != null); - const p2 = publishToPreferred(event2, user.pubkey, expiryDays != null); - return Promise.all([p1, p2]); } -- cgit v1.2.3 From d50464203973c14d434955ee132f8c656bbec8aa Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Thu, 19 Feb 2026 13:23:39 -0500 Subject: Avoid error messages on setting chat to null --- src/routes/(app)/dm/+page.svelte | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src') diff --git a/src/routes/(app)/dm/+page.svelte b/src/routes/(app)/dm/+page.svelte index 71dd9a9d..399a131b 100644 --- a/src/routes/(app)/dm/+page.svelte +++ b/src/routes/(app)/dm/+page.svelte @@ -159,6 +159,10 @@ } selectedChat = c; + if (!selectedChat) { + return; + } + if (!includesPubkey(chats, c.pubkey)) { chats.unshift(c); } -- cgit v1.2.3 From 5c3d01a8b20c670c50063fba581abbdecd9b3633 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Fri, 20 Feb 2026 11:57:05 -0500 Subject: Clear search query when opening search box --- src/routes/(app)/dm/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/routes/(app)/dm/+page.svelte b/src/routes/(app)/dm/+page.svelte index 399a131b..81ade180 100644 --- a/src/routes/(app)/dm/+page.svelte +++ b/src/routes/(app)/dm/+page.svelte @@ -445,7 +445,7 @@

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

{#if creatingNewChat} -- cgit v1.2.3 From 7f5b762ba56ae731ae1c81ee49e70d11d2e95477 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 24 Feb 2026 11:07:19 -0500 Subject: Fix bug that prevented sending nostr DMs with sk --- src/lib/nip17.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src') diff --git a/src/lib/nip17.ts b/src/lib/nip17.ts index b8352d1e..acfa8fc5 100644 --- a/src/lib/nip17.ts +++ b/src/lib/nip17.ts @@ -319,7 +319,7 @@ const createAndPublishMessageEvent = async (sk: Uint8Array, message: string, use event = await createNIP17MessageNIP07( message, userPK, recipientPK, eventFor, expiryDays); } else { - event1 = createNIP17MessageSK( + event = createNIP17MessageSK( message, sk, recipientPK, eventFor, expiryDays); } @@ -329,7 +329,6 @@ const createAndPublishMessageEvent = async (sk: Uint8Array, message: string, use // Sends a NIP-17 message from `user` to `recipient`. // If expiryDays is set, messages will expire after that many days. export const send = async (message: string, user: object, recipient: object, expiryDays?: number) => { - let event1, event2; const sk = await ensureSigner(user.pubkey); // Store sk for auth handler -- cgit v1.2.3 From 017677414e1dd9c73c339fccea55a41376e863d2 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 24 Feb 2026 11:37:26 -0500 Subject: Always use ensureSigner for nostr signing This fixes a problem that occurs with muting users using secret keys. --- src/lib/nip07.ts | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++------ src/lib/nip17.ts | 62 +-------------------------------------------- 2 files changed, 70 insertions(+), 69 deletions(-) (limited to 'src') diff --git a/src/lib/nip07.ts b/src/lib/nip07.ts index 9f675950..ec635f24 100644 --- a/src/lib/nip07.ts +++ b/src/lib/nip07.ts @@ -1,27 +1,88 @@ -import { bytesToHex } from '@noble/hashes/utils'; +import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; import { finalizeEvent } from 'nostr-tools/pure'; import { unwrapEvent } from 'nostr-tools/nip17'; -import { getPrivateKey } from '$lib/nostr'; import * as libnip44 from '$lib/nip44'; import { u } from '$lib/nip44'; +import { eventToSign, signer } from '$lib/store'; +import { get } from 'svelte/store'; + +// Ensures we have access to a secret key, triggering the signer modal if needed. +// Returns the sk as Uint8Array, or null if using NIP-07 extension. +export const ensureSigner = async (userPubkey: string): Promise => { + // Check if NIP-07 extension is available and matches user + if (window.nostr) { + try { + const extPubkey = await window.nostr.getPublicKey(); + if (extPubkey === userPubkey) { + return null; // Use NIP-07 + } + } catch (e) { + // Extension didn't work, fall through to signer + } + } + + // Check if signer already has a valid sk (64 hex chars = 32 bytes) + const currentSigner = get(signer); + if (currentSigner?.ready && currentSigner?.params?.sk && currentSigner.params.sk.length === 64) { + return hexToBytes(currentSigner.params.sk); + } + + // Clear invalid signer state before triggering modal + if (currentSigner?.ready && (!currentSigner?.params?.sk || currentSigner.params.sk.length !== 64)) { + signer.set(undefined); + } + + // Trigger the signer modal with a dummy event + const dummyEvent = { kind: 14, content: 'DM encryption', tags: [], created_at: Math.floor(Date.now() / 1000) }; + eventToSign.set(dummyEvent); + + // Wait for signer to be ready + const signerValue = await new Promise((resolve, reject) => { + let unsubscribe: () => void; + unsubscribe = signer.subscribe((v) => { + if (v === 'cancel') { + setTimeout(() => unsubscribe?.()); + eventToSign.set(null); + reject(new Error('cancelled')); + } + if (v?.ready) { + setTimeout(() => unsubscribe?.()); + eventToSign.set(null); + resolve(v); + } + }); + }); + + if (signerValue?.params?.sk) { + return hexToBytes(signerValue.params.sk); + } + + // If extension method was selected + if (signerValue?.method === 'extension') { + return null; // Use NIP-07 + } + + throw new Error('No signing method available'); +} + // Signs an event using NIP-07 if available, otherwise getPrivateKey() export const signEvent = async (event: object, user: object): object => { - if (await window.nostr.getPublicKey() === user.pubkey) { + const sk = await ensureSigner(user.pubkey); + if (sk === null) { return window.nostr.signEvent(event); } else { - const sk = await getPrivateKey(); return finalizeEvent(event, sk); } } // Encrypts an event in NIP-44 using NIP-07 if available, otherwise getPrivateKey() export const encrypt = async (json: object, user: object, receiverPK: string): string => { - if (await window.nostr.getPublicKey() === user.pubkey) { + const senderSK = await ensureSigner(user.pubkey); + if (senderSK === null) { return window.nostr.nip44.encrypt( receiverPK, JSON.stringify(json)); } else { - const senderSK = await getPrivateKey(); const senderSKString = bytesToHex(senderSK); const ckey = u.getConversationKey(senderSKString, receiverPK); return libnip44.encrypt(JSON.stringify(json), ckey); @@ -30,11 +91,11 @@ export const encrypt = async (json: object, user: object, receiverPK: string): s // Decrypts an event in NIP-44 using NIP-07 if available, otherwise getPrivateKey() export const decrypt = async (encrypted: string, user: object, receiverPK: string): object => { - if (await window.nostr.getPublicKey() === user.pubkey) { + const senderSK = await ensureSigner(user.pubkey); + if (senderSK === null) { return JSON.parse(await window.nostr.nip44.decrypt( receiverPK, encrypted)); } else { - const senderSK = await getPrivateKey(); const senderSKString = bytesToHex(senderSK); const ckey = u.getConversationKey(senderSKString, receiverPK); return JSON.parse(libnip44.decrypt(encrypted, ckey)); diff --git a/src/lib/nip17.ts b/src/lib/nip17.ts index acfa8fc5..70741786 100644 --- a/src/lib/nip17.ts +++ b/src/lib/nip17.ts @@ -5,11 +5,10 @@ import { SimplePool } from 'nostr-tools/pool'; import { Relay } from 'nostr-tools/relay'; import { unwrapEvent } from 'nostr-tools/nip17'; +import { ensureSigner } from '$lib/nip07'; import { relaysSupporting } from '$lib/nip11'; import { expired, expiration } from '$lib/nip40'; import { encrypt, u } from '$lib/nip44'; -import { eventToSign, signer } from '$lib/store'; -import { get } from 'svelte/store'; const TWO_DAYS = 2 * 24 * 60 * 60; const randomTimestamp = () => Math.floor( @@ -21,65 +20,6 @@ import { PUBLIC_DM_RELAYS } from '$env/static/public'; const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(','); const DM_FETCH_LIMIT = 65536; -// Ensures we have access to a secret key, triggering the signer modal if needed. -// Returns the sk as Uint8Array, or null if using NIP-07 extension. -const ensureSigner = async (userPubkey: string): Promise => { - // Check if NIP-07 extension is available and matches user - if (window.nostr) { - try { - const extPubkey = await window.nostr.getPublicKey(); - if (extPubkey === userPubkey) { - return null; // Use NIP-07 - } - } catch (e) { - // Extension didn't work, fall through to signer - } - } - - // Check if signer already has a valid sk (64 hex chars = 32 bytes) - const currentSigner = get(signer); - if (currentSigner?.ready && currentSigner?.params?.sk && currentSigner.params.sk.length === 64) { - return hexToBytes(currentSigner.params.sk); - } - - // Clear invalid signer state before triggering modal - if (currentSigner?.ready && (!currentSigner?.params?.sk || currentSigner.params.sk.length !== 64)) { - signer.set(undefined); - } - - // Trigger the signer modal with a dummy event - const dummyEvent = { kind: 14, content: 'DM encryption', tags: [], created_at: Math.floor(Date.now() / 1000) }; - eventToSign.set(dummyEvent); - - // Wait for signer to be ready - const signerValue = await new Promise((resolve, reject) => { - let unsubscribe: () => void; - unsubscribe = signer.subscribe((v) => { - if (v === 'cancel') { - setTimeout(() => unsubscribe?.()); - eventToSign.set(null); - reject(new Error('cancelled')); - } - if (v?.ready) { - setTimeout(() => unsubscribe?.()); - eventToSign.set(null); - resolve(v); - } - }); - }); - - if (signerValue?.params?.sk) { - return hexToBytes(signerValue.params.sk); - } - - // If extension method was selected - if (signerValue?.method === 'extension') { - return null; // Use NIP-07 - } - - throw new Error('No signing method available'); -} - // Create a NIP-17 message using a provided sender secret key. // If wrapPK is specified and different than receiverPK, // the message will be gift-wrapped to wrapPK -- cgit v1.2.3 From 8ab48973812289c19f48d15b7a9771a89d35797a Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Tue, 24 Feb 2026 12:00:06 -0500 Subject: Clear DM text entry upon sending DM --- src/routes/(app)/dm/+page.svelte | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/routes/(app)/dm/+page.svelte b/src/routes/(app)/dm/+page.svelte index 81ade180..8861aa9d 100644 --- a/src/routes/(app)/dm/+page.svelte +++ b/src/routes/(app)/dm/+page.svelte @@ -269,6 +269,7 @@ const sendMessage = async (message: string) => { const expiry = expiryEnabled ? expiryDays : null; await libnip17.send(message, user, selectedChat, expiry); + text = ""; updateEvents(); } -- cgit v1.2.3