From 696f64df3f484af1b367a0c2f01aa7fb0cfeee34 Mon Sep 17 00:00:00 2001 From: highperfocused Date: Sun, 6 Sep 2026 17:55:42 +0200 Subject: [PATCH] fix: don't treat mention-only e tags as replies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review: isReply() flagged any e tag as a reply, including one marked "mention" — a citation, not a thread reply per NIP-10. That would have hidden quote-notes from the Feed/Profile timelines they belong in. Also fixed rootReference()'s docstring, which claimed to fall back to the *last* positional e tag when the code (correctly) uses the first. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto --- src/lib/nostrUtils.test.ts | 15 +++++++++++++++ src/lib/nostrUtils.ts | 11 +++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/lib/nostrUtils.test.ts b/src/lib/nostrUtils.test.ts index b18c721..afe93c8 100644 --- a/src/lib/nostrUtils.test.ts +++ b/src/lib/nostrUtils.test.ts @@ -26,6 +26,21 @@ describe('isReply', () => { it('is true for a note using the deprecated positional e tag', () => { expect(isReply(note([['e', 'parent-id']]))).toBe(true); }); + + it('is false for a note that only mentions another event', () => { + expect(isReply(note([['e', 'mentioned-id', '', 'mention']]))).toBe(false); + }); + + it('is true for a note with a mention alongside a marked reply', () => { + expect( + isReply( + note([ + ['e', 'root-id', '', 'root'], + ['e', 'mentioned-id', '', 'mention'], + ]), + ), + ).toBe(true); + }); }); describe('rootReference', () => { diff --git a/src/lib/nostrUtils.ts b/src/lib/nostrUtils.ts index 7b1247d..7477925 100644 --- a/src/lib/nostrUtils.ts +++ b/src/lib/nostrUtils.ts @@ -93,7 +93,8 @@ export function tagValues(event: NostrEvent, name: string): string[] { /** * The event a reply points at, following NIP-10: prefer an explicit `root` - * marker, fall back to the last positional `e` tag. + * marker, fall back to the first positional `e` tag (the deprecated scheme + * puts the root id first: `["e", ], ["e", ]`). */ export function rootReference(event: NostrEvent): string | undefined { const marked = event.tags.find(([name, , , marker]) => name === 'e' && marker === 'root'); @@ -102,6 +103,12 @@ export function rootReference(event: NostrEvent): string | undefined { return positional[0]?.[1]; } +/** + * True for a reply per NIP-10: a marked `root`/`reply` `e` tag, or an + * unmarked one (the deprecated positional scheme). An `e` tag marked + * `mention` alone does not make an event a reply — it cites another event + * without being part of its thread. + */ export function isReply(event: NostrEvent): boolean { - return event.tags.some(([name]) => name === 'e'); + return event.tags.some(([name, , , marker]) => name === 'e' && marker !== 'mention'); }