aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-02 19:03:19 -0700
committerAdam Soltys <[email protected]>2026-09-02 19:03:19 -0700
commita2069d6ad1423b1788428b93b6bbb4531c551572 (patch)
tree5000f9a25b508e3febaea6921ba85c679f35f213 /src
parent15d8c6f3dab19c7f51ad00b2fc377bf6eaa7f819 (diff)
downloadcoinosv3-a2069d6ad1423b1788428b93b6bbb4531c551572.tar.gz
coinosv3-a2069d6ad1423b1788428b93b6bbb4531c551572.zip
nevent and nprofile references come alive: nprofile mentions render as clickable @names beside their npub siblings, and note/nevent references become 'view note' links that fetch the event by id (relay hints first) and open its thread right in the app — the dim truncated stubs survive only for what genuinely can't resolve
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
Diffstat (limited to 'src')
-rw-r--r--src/features/messages.js29
-rw-r--r--src/i18n.js3
-rw-r--r--src/nostr.js12
3 files changed, 39 insertions, 5 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index ca56335..1daa5bc 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -14,7 +14,7 @@
import {
subscribeOn, publishOn, queryOn, fetchNostrProfile, fetchInboxRelays,
- npubOf, parseNostrPubkey, generateSecretKey, getPublicKey, finalizeEvent, nip44,
+ npubOf, parseNostrPubkey, parseNostrRef, generateSecretKey, getPublicKey, finalizeEvent, nip44,
PROFILE_RELAYS,
} from '../nostr.js';
import {
@@ -1517,10 +1517,18 @@ export function messagesFeature(ctx) {
out.push(h('a', { href: part, target: '_blank', rel: 'noopener noreferrer' },
part.length > 64 ? part.slice(0, 61) + '…' : part));
}
- } else if (/^nostr:npub1/i.test(part)) {
- const mpk = parseNostrPubkey(part.slice(6));
- if (mpk) out.push(h('a', { href: '#', onClick: (e) => { e.preventDefault(); openProfile(mpk); } }, '@' + displayName(mpk)));
- else out.push(part);
+ } else if (/^nostr:(npub|nprofile)1/i.test(part)) {
+ const ref = parseNostrRef(part.slice(6));
+ if (ref && ref.type === 'pubkey') out.push(h('a', { href: '#', onClick: (e) => { e.preventDefault(); openProfile(ref.pk); } }, '@' + displayName(ref.pk)));
+ else out.push(h('span', { class: 'faint' }, part.slice(6, 18) + '…'));
+ } else if (/^nostr:(note|nevent)1/i.test(part)) {
+ // a referenced note opens right here as a thread — fetched by id
+ // (with the reference's relay hints) when tapped
+ const ref = parseNostrRef(part.slice(6));
+ if (ref && ref.type === 'event') out.push(h('a', {
+ href: '#', onClick: (e) => { e.preventDefault(); openNoteRef(ref); },
+ }, t('noteRefLink')));
+ else out.push(h('span', { class: 'faint' }, part.slice(6, 18) + '…'));
} else if (/^nostr:/i.test(part)) {
out.push(h('span', { class: 'faint' }, part.slice(6, 18) + '…'));
} else out.push(part);
@@ -1637,6 +1645,17 @@ export function messagesFeature(ctx) {
ui.noteThread = { rootId: rootIdOf(ev), focusId: ev.id, seed: ev };
render();
}
+ // A nostr:nevent / nostr:note reference: the thread loader needs the real
+ // event (its tags name the root, its author names the home relays), so
+ // fetch it by id — the reference's relay hints first — then open.
+ async function openNoteRef(ref) {
+ toast(t('noteRefLoading'));
+ const relays = [...new Set([...(ref.relays || []), ...NOTE_RELAYS])];
+ const evs = await queryOn(relays, { ids: [ref.id] }, 4000).catch(() => []);
+ const ev = (evs || [])[0];
+ if (ev) openNoteThread(ev);
+ else toast(t('noteRefNotFound'));
+ }
// Publish a kind-1 reply to the focused note (NIP-10 markers), addressed to
// the conversation's own relays plus ours, and shown optimistically.
async function publishReply(c, s, text) {
diff --git a/src/i18n.js b/src/i18n.js
index 14908bc..1d0a9d3 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -180,6 +180,9 @@ const STR = {
msgNoIdentity: "Your nostr signer isn't connected — reconnect it to send as yourself",
msgSendFailed: 'No relay accepted the message',
msgLockedChat: 'Your wallet is locked, so it can’t sign messages.',
+ noteRefLink: 'view note',
+ noteRefLoading: 'Fetching note…',
+ noteRefNotFound: "Couldn't find that note on the relays.",
msgSignerOff: "Your nostr signer isn't connected, so you can't post as yourself yet.",
msgReconnect: 'Reconnect',
msgDmsTitle: 'Messages',
diff --git a/src/nostr.js b/src/nostr.js
index 753d4a8..a76ef5c 100644
--- a/src/nostr.js
+++ b/src/nostr.js
@@ -120,6 +120,18 @@ export function parseNostrPubkey(input) {
}
// An npub or 64-hex nostr pubkey → hex, or null if it isn't one.
export function npubOf(pkHex) { try { return npubEncode(pkHex); } catch { return null; } }
+// Any nostr entity reference → something renderable: npub/nprofile land as a
+// pubkey (with relay hints when carried), note/nevent as an event id.
+export function parseNostrRef(input) {
+ try {
+ const d = nip19decode(String(input || '').trim());
+ if (d.type === 'npub') return { type: 'pubkey', pk: d.data };
+ if (d.type === 'nprofile') return { type: 'pubkey', pk: d.data.pubkey, relays: d.data.relays || [] };
+ if (d.type === 'note') return { type: 'event', id: d.data, relays: [] };
+ if (d.type === 'nevent') return { type: 'event', id: d.data.id, relays: d.data.relays || [], author: d.data.author || null };
+ } catch {}
+ return null;
+}
// A raw secret key → nsec, for the one place that deliberately exports it.
export function nsecOf(sk) { try { return nsecEncode(sk); } catch { return null; } }