diff options
| author | Adam Soltys <[email protected]> | 2026-09-21 18:17:25 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-21 18:17:25 -0700 |
| commit | b00953e0a69df5d95cf6dd792b63c3671bd30191 (patch) | |
| tree | 751a7c72059baae3dc2c809f4b7258026e02fcdc /src/features | |
| parent | 562a52cda54a97f1595fbcdb5803d7c49421b825 (diff) | |
| download | coinosv3-b00953e0a69df5d95cf6dd792b63c3671bd30191.tar.gz coinosv3-b00953e0a69df5d95cf6dd792b63c3671bd30191.zip | |
Threads: climb to the real root, and look for it on the right relays.
A client that tags only the note it answered leaves the root unnamed, so
the note it replied to was taken as the root and the thread opened with
its first post missing (a reply at the top wearing a "reply" badge). The
loader now climbs: when the note it resolved as the root is itself a
reply, it fetches that note's root and re-roots the thread, a few hops at
most. The root is also looked for on the participants' relays and the e
tags' relay hints, not just the reply author's.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/messages.js | 47 |
1 files changed, 39 insertions, 8 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 3f748f4..059de7a 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -5099,20 +5099,51 @@ export function messagesFeature(ctx) { c = { status: 'loading', rootId, root: seed.id === rootId ? seed : null, replies: seed.id === rootId ? [] : [seed] }; threadCache.set(rootId, c); (async () => { - // the conversation's home relays are the root author's (NIP-10 outbox); - // the seed's author is the best guess until the root is known - const relays = await notesRelays((c.root || seed).pubkey); + // Where a thread lives: the reply's author, the people it tags (the + // root's author is usually first among them), the relay hints on its + // e tags, and ours. The reply author's relays alone missed roots + // written elsewhere — the thread then opened without its first post. + const relaysFor = async (ev) => { + const hints = ev.tags.filter((x) => x[0] === 'e' && /^wss?:\/\//i.test(x[2] || '')).map((x) => x[2]); + const people = [ev.pubkey, ...ev.tags.filter((x) => x[0] === 'p').map((x) => x[1]).slice(0, 3)]; + const sets = await Promise.all(people.map((pk) => notesRelays(pk).catch(() => []))); + return [...new Set([...hints, ...sets.flat(), ...NOTE_RELAYS])].slice(0, 12); + }; + let relays = await relaysFor(c.root || seed); const [roots, replies] = await Promise.all([ c.root ? Promise.resolve([]) : queryOn(relays, { kinds: [1], ids: [rootId] }, 4000), queryOn(relays, { kinds: [1], '#e': [rootId], limit: 80 }, 4500), ]); if (!c.root) c.root = (roots || [])[0] || null; - const seen = new Set([rootId]); - c.replies = [...c.replies, ...(replies || [])] - .filter((e) => !seen.has(e.id) && seen.add(e.id)) + let all = [...c.replies, ...(replies || [])]; + // A client that tags only the note it answered leaves the root + // unnamed: what we resolved as the root is itself a reply. Climb — + // fetch ITS root and re-root the thread there — a few hops at most. + let top = c.root, topId = rootId; + for (let hop = 0; top && hop < 4; hop++) { + const up = rootIdOf(top); + if (up === top.id) break; + relays = [...new Set([...relays, ...(await relaysFor(top))])].slice(0, 14); + const [ups, more] = await Promise.all([ + queryOn(relays, { kinds: [1], ids: [up] }, 4000), + queryOn(relays, { kinds: [1], '#e': [up], limit: 80 }, 4500), + ]); + const upNote = (ups || [])[0]; + if (!upNote) break; + all = [...all, top, ...(more || [])]; + top = upNote; topId = up; + } + if (topId !== rootId) { + c.root = top; c.rootId = topId; + threadCache.set(topId, c); + if (ui.noteThread && ui.noteThread.rootId === rootId) ui.noteThread.rootId = topId; + } + const seen = new Set([c.rootId]); + c.replies = all + .filter((e) => e && e.id !== c.rootId && !seen.has(e.id) && seen.add(e.id)) .sort((a, b) => a.created_at - b.created_at); c.status = 'ready'; - if (ui.noteThread && ui.noteThread.rootId === rootId) { + if (ui.noteThread && ui.noteThread.rootId === c.rootId) { render(); // The inline reply box may have MOVED on this render (it slots under // the focused note once that note exists) — a focus taken before the @@ -5126,7 +5157,7 @@ export function messagesFeature(ctx) { } })().catch(() => { c.status = 'ready'; - if (ui.noteThread && ui.noteThread.rootId === rootId) render(); + if (ui.noteThread && ui.noteThread.rootId === c.rootId) render(); }); return c; } |
