|
|
|
|
@@ -575,8 +575,11 @@
|
|
|
|
|
if (!evt?.tags) return false;
|
|
|
|
|
const eTags = evt.tags.filter((t) => t[0] === 'e' && t[1]);
|
|
|
|
|
if (eTags.length === 0) return false;
|
|
|
|
|
// A reply is any kind 1 that references the target event id in an e tag.
|
|
|
|
|
return eTags.some((t) => t[1] === targetEventId);
|
|
|
|
|
// A reply is any kind 1 that references the root post OR any already-known
|
|
|
|
|
// reply in the thread (nested replies only carry their immediate parent,
|
|
|
|
|
// not the root post id). This keeps the thread inclusive of replies-to-replies.
|
|
|
|
|
if (eTags.some((t) => t[1] === targetEventId)) return true;
|
|
|
|
|
return eTags.some((t) => replyIds.has(t[1]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function upsertReply(evt) {
|
|
|
|
|
@@ -850,11 +853,6 @@
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendReplyIfNew(evt) {
|
|
|
|
|
if (!upsertReply(evt)) return;
|
|
|
|
|
renderReplies();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ================================================================
|
|
|
|
|
BOTTOM REPLY COMPOSER
|
|
|
|
|
================================================================
|
|
|
|
|
@@ -964,27 +962,104 @@
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ----------------------------------------------------------------
|
|
|
|
|
fetchRepliesRecursive(rootEventId, maxDepth)
|
|
|
|
|
----------------------------------------------------------------
|
|
|
|
|
Recursively fetches replies to the root post AND replies to those
|
|
|
|
|
replies (nested replies), up to maxDepth levels. Nested replies
|
|
|
|
|
only carry their immediate parent in their `e` tags — not the root
|
|
|
|
|
post id — so a single { '#e': [rootId] } query misses them.
|
|
|
|
|
|
|
|
|
|
Strategy (breadth-first by depth level):
|
|
|
|
|
depth 0: fetch replies to rootEventId
|
|
|
|
|
depth 1: fetch replies to every reply found at depth 0
|
|
|
|
|
depth 2: fetch replies to every reply found at depth 1
|
|
|
|
|
... up to maxDepth
|
|
|
|
|
|
|
|
|
|
The `#e` filter accepts an array of ids, so we batch all ids at
|
|
|
|
|
the current depth into a single fetch. Dedup via replyIds so a
|
|
|
|
|
reply discovered via multiple parent paths is only added once.
|
|
|
|
|
|
|
|
|
|
Both the cache (queryCache) and relays (ndkFetchEvents) are
|
|
|
|
|
consulted at each level — cache first for fast display, then
|
|
|
|
|
relays for hydration. A per-level timeout guards against
|
|
|
|
|
excessive relay requests on very long threads.
|
|
|
|
|
---------------------------------------------------------------- */
|
|
|
|
|
const REPLY_FETCH_MAX_DEPTH = 5;
|
|
|
|
|
const REPLY_FETCH_LEVEL_TIMEOUT_MS = 8000;
|
|
|
|
|
|
|
|
|
|
function fetchRepliesForLevelFromCache(parentIds) {
|
|
|
|
|
return queryCache({ kinds: [1], '#e': parentIds }).catch(() => []).then((r) => Array.isArray(r) ? r : []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fetchRepliesForLevelFromRelays(parentIds) {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
let settled = false;
|
|
|
|
|
const timer = setTimeout(() => {
|
|
|
|
|
if (settled) return;
|
|
|
|
|
settled = true;
|
|
|
|
|
resolve([]);
|
|
|
|
|
}, REPLY_FETCH_LEVEL_TIMEOUT_MS);
|
|
|
|
|
|
|
|
|
|
ndkFetchEvents({ kinds: [1], '#e': parentIds })
|
|
|
|
|
.then((events) => {
|
|
|
|
|
if (settled) return;
|
|
|
|
|
settled = true;
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
resolve(Array.isArray(events) ? events : []);
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
console.warn('[post-feed.html] Recursive reply level fetch failed:', error?.message || error);
|
|
|
|
|
if (settled) return;
|
|
|
|
|
settled = true;
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
resolve([]);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function fetchRepliesRecursive(rootEventId, maxDepth = REPLY_FETCH_MAX_DEPTH) {
|
|
|
|
|
if (!rootEventId) return;
|
|
|
|
|
|
|
|
|
|
let currentLevelIds = [rootEventId];
|
|
|
|
|
|
|
|
|
|
for (let depth = 0; depth < maxDepth; depth += 1) {
|
|
|
|
|
if (currentLevelIds.length === 0) break;
|
|
|
|
|
|
|
|
|
|
// Cache first — fast display of whatever we already have.
|
|
|
|
|
const cachedEvents = await fetchRepliesForLevelFromCache(currentLevelIds);
|
|
|
|
|
const nextLevelIds = [];
|
|
|
|
|
for (const evt of cachedEvents) {
|
|
|
|
|
if (upsertReply(evt)) {
|
|
|
|
|
nextLevelIds.push(evt.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (cachedEvents.length > 0) renderReplies();
|
|
|
|
|
|
|
|
|
|
// Then relay hydration for this level.
|
|
|
|
|
const relayEvents = await fetchRepliesForLevelFromRelays(currentLevelIds);
|
|
|
|
|
for (const evt of relayEvents) {
|
|
|
|
|
if (upsertReply(evt)) {
|
|
|
|
|
nextLevelIds.push(evt.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (relayEvents.length > 0) renderReplies();
|
|
|
|
|
|
|
|
|
|
// Move to the next depth level: replies to the replies we just found.
|
|
|
|
|
// (upsertReply dedupes via replyIds, so ids already seen are skipped
|
|
|
|
|
// on the next iteration by virtue of not being newly added — but we
|
|
|
|
|
// still need to query for their children, so include all newly-found
|
|
|
|
|
// ids here.)
|
|
|
|
|
currentLevelIds = nextLevelIds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Final render to ensure consistency.
|
|
|
|
|
renderReplies();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function fetchReplies() {
|
|
|
|
|
if (!targetEventId) return;
|
|
|
|
|
|
|
|
|
|
// Cache first.
|
|
|
|
|
try {
|
|
|
|
|
const cachedReplies = await queryCache({ kinds: [1], '#e': [targetEventId] });
|
|
|
|
|
if (Array.isArray(cachedReplies)) {
|
|
|
|
|
cachedReplies.forEach((evt) => upsertReply(evt));
|
|
|
|
|
renderReplies();
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
|
|
|
|
// Then relay fetch.
|
|
|
|
|
void ndkFetchEvents({ kinds: [1], '#e': [targetEventId] }).then((fresh) => {
|
|
|
|
|
if (Array.isArray(fresh)) {
|
|
|
|
|
fresh.forEach((evt) => upsertReply(evt));
|
|
|
|
|
}
|
|
|
|
|
renderReplies();
|
|
|
|
|
}).catch((error) => {
|
|
|
|
|
console.warn('[post-feed.html] Reply fetch failed:', error?.message || error);
|
|
|
|
|
});
|
|
|
|
|
await fetchRepliesRecursive(targetEventId, REPLY_FETCH_MAX_DEPTH);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ================================================================
|
|
|
|
|
@@ -1153,7 +1228,16 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isReplyToThread(evt)) {
|
|
|
|
|
appendReplyIfNew(evt);
|
|
|
|
|
const wasNew = upsertReply(evt);
|
|
|
|
|
if (wasNew) {
|
|
|
|
|
renderReplies();
|
|
|
|
|
// A newly-seen reply may itself have nested replies we haven't
|
|
|
|
|
// fetched yet. Trigger a one-level-deep fetch for replies to it
|
|
|
|
|
// so nested replies appear in realtime too.
|
|
|
|
|
fetchRepliesRecursive(evt.id, 1).catch((err) => {
|
|
|
|
|
console.warn('[post-feed.html] Realtime nested fetch failed:', err?.message || err);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|