aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-21 18:17:25 -0700
committerAdam Soltys <[email protected]>2026-09-21 18:17:25 -0700
commitb00953e0a69df5d95cf6dd792b63c3671bd30191 (patch)
tree751a7c72059baae3dc2c809f4b7258026e02fcdc /src
parent562a52cda54a97f1595fbcdb5803d7c49421b825 (diff)
downloadcoinosv3-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')
-rw-r--r--src/features/messages.js47
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;
}