Skip to content

Commit 6d2025f

Browse files
committed
nested replies
1 parent b4be1f3 commit 6d2025f

2 files changed

Lines changed: 62 additions & 20 deletions

File tree

src/routes/thread/[id]/+page.svelte

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
2020
const initialThreadReplyLimit = 40;
2121
const nestedThreadReplyLimit = 80;
22+
const nestedThreadReplyDepth = 2;
2223
const threadReplyPageLimit = 40;
2324
const maxThreadReplies = 160;
2425
const threadScrollStateMaxAgeMs = 30 * 60 * 1000;
@@ -58,10 +59,10 @@
5859
$: threadEvents = mergeThreadEvents(rootEvent ? [rootEvent, ...localThreadEvents] : localThreadEvents, []);
5960
$: root = rootEvent?.id === id ? rootEvent : threadEvents.find((event) => event.id === id);
6061
$: replies = id ? threadReplyEvents(threadEvents, id) : [];
61-
$: sortedReplies = [...replies].sort((a, b) => a.created_at - b.created_at);
62+
$: threadReplyItems = id ? threadReplyTreeItems(threadEvents, id) : [];
6263
$: hiddenThreadQuoteIds = id ? sameThreadQuotedNoteIds(id, threadEvents) : [];
6364
$: if (browser && routeKey && root && routeKey !== restoredThreadRouteKey) void restoreThreadScrollPosition(routeKey);
64-
$: if (browser && routeKey && focusedReplyId && sortedReplies.some((reply) => reply.id === focusedReplyId) && `${routeKey}:${focusedReplyId}` !== focusedThreadRouteKey) {
65+
$: if (browser && routeKey && focusedReplyId && threadReplyItems.some((item) => item.event.id === focusedReplyId) && `${routeKey}:${focusedReplyId}` !== focusedThreadRouteKey) {
6566
void scrollFocusedReplyIntoView(routeKey, focusedReplyId);
6667
}
6768
$: if (browser) syncThreadLiveSubscription(root?.id ?? '', [root?.id ?? '', ...replies.map((reply) => reply.id)]);
@@ -125,6 +126,34 @@
125126
return nonRoot?.[1] ?? (eTags.some((tag) => tag[1] === rootId) ? rootId : '');
126127
}
127128
129+
function threadReplyTreeItems(items: NostrEvent[], rootId: string) {
130+
const replyItems = threadReplyEvents(items, rootId);
131+
const replyById = new Map(replyItems.map((event) => [event.id, event]));
132+
const childrenByParent = new Map<string, NostrEvent[]>();
133+
const sorted = [...replyItems].sort((a, b) => a.created_at - b.created_at);
134+
for (const event of sorted) {
135+
const parentId = replyParentId(event, rootId);
136+
if (!parentId) continue;
137+
if (parentId !== rootId && !replyById.has(parentId)) continue;
138+
const children = childrenByParent.get(parentId) ?? [];
139+
children.push(event);
140+
childrenByParent.set(parentId, children);
141+
}
142+
143+
const output: Array<{ event: NostrEvent; depth: number }> = [];
144+
const visited = new Set<string>();
145+
const appendChildren = (parentId: string, depth: number) => {
146+
for (const child of childrenByParent.get(parentId) ?? []) {
147+
if (visited.has(child.id)) continue;
148+
visited.add(child.id);
149+
output.push({ event: child, depth });
150+
appendChildren(child.id, depth + 1);
151+
}
152+
};
153+
appendChildren(rootId, 0);
154+
return output;
155+
}
156+
128157
async function hydrateThread() {
129158
if (!id || hydratedId === id) return;
130159
const rootId = id;
@@ -216,13 +245,23 @@
216245
}
217246
218247
async function fetchNestedThreadReplies(parentReplies: NostrEvent[]) {
219-
return parentReplies.length
220-
? fetchThreadReplies(
221-
parentReplies.map((event) => event.id),
222-
$relays,
223-
nestedThreadReplyLimit
224-
).catch(() => [])
225-
: [];
248+
const nestedReplies: NostrEvent[] = [];
249+
let parents = parentReplies;
250+
const known = new Set(parentReplies.map((event) => event.id));
251+
for (let depth = 0; depth < nestedThreadReplyDepth && parents.length && nestedReplies.length < nestedThreadReplyLimit; depth += 1) {
252+
const remaining = nestedThreadReplyLimit - nestedReplies.length;
253+
const next = await fetchThreadReplies(
254+
parents.map((event) => event.id),
255+
$relays,
256+
remaining
257+
).catch(() => []);
258+
if (!next.length) break;
259+
const fresh = next.filter((event) => !known.has(event.id));
260+
fresh.forEach((event) => known.add(event.id));
261+
nestedReplies.push(...fresh);
262+
parents = fresh;
263+
}
264+
return nestedReplies;
226265
}
227266
228267
async function fetchThreadReplyWindow(options: { limit?: number; since?: number; until?: number } = {}) {
@@ -681,9 +720,11 @@
681720
{#if root}
682721
<div class="feed-list">
683722
<NoteCard event={root} profile={$profiles[root.pubkey]} hiddenQuotedNoteIds={hiddenThreadQuoteIds} initialExpanded onOpen={openThreadNote} />
684-
{#if replies.length}
685-
{#each sortedReplies as reply (reply.id)}
686-
<NoteCard event={reply} profile={$profiles[reply.pubkey]} featured={reply.id === focusedReplyId} hiddenQuotedNoteIds={hiddenThreadQuoteIds} onOpen={openThreadNote} />
723+
{#if threadReplyItems.length}
724+
{#each threadReplyItems as item (item.event.id)}
725+
<div class="thread-reply" class:nested={item.depth > 0} style={`--thread-depth: ${Math.min(item.depth, 3)}`}>
726+
<NoteCard event={item.event} profile={$profiles[item.event.pubkey]} featured={item.event.id === focusedReplyId} hiddenQuotedNoteIds={hiddenThreadQuoteIds} onOpen={openThreadNote} />
727+
</div>
687728
{/each}
688729
{:else if loading}
689730
<div class="empty-state"><span>Loading replies</span></div>

src/styles.css

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2458,18 +2458,18 @@ label span, label strong { color: var(--text); }
24582458
min-width: 0;
24592459
max-width: 100%;
24602460
}
2461-
.thread-reply.direct {
2462-
margin-left: 10px;
2463-
}
24642461
.thread-reply.nested {
2465-
margin-left: 0;
2466-
padding-left: 0;
2467-
border-left: 0;
2462+
margin-left: min(24px, calc(var(--thread-depth, 1) * 9px));
2463+
padding-left: 8px;
2464+
border-left: 2px solid color-mix(in srgb, var(--line) 74%, transparent);
24682465
}
24692466
.thread-reply .note-card {
24702467
min-width: 0;
24712468
max-width: 100%;
24722469
}
2470+
.thread-reply.nested .note-card {
2471+
padding-top: 8px;
2472+
}
24732473
.thread-back {
24742474
width: fit-content;
24752475
min-height: 36px;
@@ -3096,8 +3096,9 @@ label span, label strong { color: var(--text); }
30963096
.thread-page {
30973097
overflow-x: hidden;
30983098
}
3099-
.thread-reply.direct {
3100-
margin-left: 6px;
3099+
.thread-reply.nested {
3100+
margin-left: min(18px, calc(var(--thread-depth, 1) * 7px));
3101+
padding-left: 6px;
31013102
}
31023103
.messages-view {
31033104
height: calc(100svh - 64px - env(safe-area-inset-bottom));

0 commit comments

Comments
 (0)