Compare commits

..

2 Commits

Author SHA1 Message Date
Laan Tungir
0267233f87 Fix post-feed.html reply composer formatting — add missing .topPostComposerInput CSS rule
The reply composer was missing the CSS rule that gives it visible formatting
(border, padding, min-height, border-radius). Copied the rule from post.html.
2026-06-25 20:29:51 -04:00
Laan Tungir
6b45092bc8 Fix post-feed.html: recursive reply fetching for nested replies
Replies to replies don't carry the root post ID in their e tags, only their
immediate parent's ID. The old single-level #e filter missed nested replies.

Added fetchRepliesRecursive() which does breadth-first recursive fetching:
- Starts with root post ID, fetches direct replies
- For each reply found, fetches replies to THAT reply
- Continues up to 5 levels deep (REPLY_FETCH_MAX_DEPTH)
- Deduplicates by event ID
- Per-level timeout (8s) to guard against slow relays
- Real-time handler also triggers 1-level recursive fetch for new replies

This matches Amethyst's ThreadFilterAssemblerSubscription approach.
2026-06-25 20:20:10 -04:00
2 changed files with 122 additions and 30 deletions

View File

@@ -1,5 +1,5 @@
{
"VERSION": "v0.7.27",
"VERSION_NUMBER": "0.7.27",
"BUILD_DATE": "2026-06-26T00:00:43.641Z"
"VERSION": "v0.7.29",
"VERSION_NUMBER": "0.7.29",
"BUILD_DATE": "2026-06-26T00:29:51.222Z"
}

View File

@@ -109,6 +109,14 @@
width: 100%;
}
.topPostComposerInput {
min-height: 6em;
padding: 8px;
line-height: 1.4;
border: 2px solid var(--primary-color);
border-radius: 10px;
}
.divPostItem .post-composer-wrapper {
width: 100%;
}
@@ -575,8 +583,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 +861,6 @@
}
}
function appendReplyIfNew(evt) {
if (!upsertReply(evt)) return;
renderReplies();
}
/* ================================================================
BOTTOM REPLY COMPOSER
================================================================
@@ -964,27 +970,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 +1236,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);
});
}
}
});