-
- {#if isReply()}
-
- {/if}
+ function getPreviewContent(): string {
+ // First 150 chars, plaintext (no markdown/images)
+ const plaintext = stripMarkdown(post.content);
+ return plaintext.slice(0, 150) + (plaintext.length > 150 ? '...' : '');
+ }
- {#if hasQuotedEvent()}
-
- {/if}
+
-
-
- {#if needsExpansion}
-
{/if}
-
-
- {getKindInfo(post.kind).number}
- {getKindInfo(post.kind).description}
-
diff --git a/src/lib/modules/threads/ThreadCard.svelte b/src/lib/modules/threads/ThreadCard.svelte
index b44da99..55972f3 100644
--- a/src/lib/modules/threads/ThreadCard.svelte
+++ b/src/lib/modules/threads/ThreadCard.svelte
@@ -5,6 +5,7 @@
import { onMount } from 'svelte';
import type { NostrEvent } from '../../types/nostr.js';
import { getKindInfo } from '../../types/kind-lookup.js';
+ import { stripMarkdown } from '../../services/text-utils.js';
interface Props {
thread: NostrEvent;
@@ -162,7 +163,7 @@
function getPreview(): string {
// First 250 chars, plaintext (no markdown/images)
- const plaintext = thread.content.replace(/[#*_`\[\]()]/g, '').replace(/\n/g, ' ');
+ const plaintext = stripMarkdown(thread.content);
return plaintext.slice(0, 250) + (plaintext.length > 250 ? '...' : '');
}
diff --git a/src/lib/modules/threads/ThreadList.svelte b/src/lib/modules/threads/ThreadList.svelte
index 7aed3f3..2e3e114 100644
--- a/src/lib/modules/threads/ThreadList.svelte
+++ b/src/lib/modules/threads/ThreadList.svelte
@@ -20,6 +20,25 @@
loadThreads();
});
+ // Re-sort threads when sortBy changes (but not when threads changes)
+ let lastSortBy = $state<'newest' | 'active' | 'upvoted' | null>(null);
+ $effect(() => {
+ // Only re-sort if sortBy actually changed
+ if (sortBy !== lastSortBy && threads.length > 0 && !loading) {
+ lastSortBy = sortBy;
+
+ if (sortBy === 'newest') {
+ threads = sortThreadsSync(threads);
+ } else {
+ // For async sorts, trigger the sort without blocking
+ // Pass sortBy explicitly to ensure we use the current value
+ sortThreads(threads, sortBy).then(sorted => {
+ threads = sorted;
+ });
+ }
+ }
+ });
+
async function loadThreads() {
loading = true;
try {
@@ -39,17 +58,23 @@
cacheResults: true,
onUpdate: async (updatedEvents) => {
// Update threads when fresh data arrives from relays
- threads = await sortThreads(updatedEvents);
+ // Pass sortBy explicitly to ensure we use the current value
+ threads = await sortThreads(updatedEvents, sortBy);
}
}
);
- // Set initial cached data immediately
- if (sortBy === 'newest') {
+ // Set initial cached data immediately using current sortBy value
+ // Capture sortBy at this point to ensure we use the correct value
+ const currentSort = sortBy;
+ if (currentSort === 'newest') {
threads = sortThreadsSync(events);
} else {
- threads = await sortThreads(events);
+ // Pass sortBy explicitly to ensure we use the current value
+ threads = await sortThreads(events, currentSort);
}
+ // Update lastSortBy to match current sort so effect doesn't re-trigger
+ lastSortBy = currentSort;
} catch (error) {
console.error('Error loading threads:', error);
threads = []; // Set empty array on error to prevent undefined issues
@@ -63,69 +88,114 @@
return [...events].sort((a, b) => b.created_at - a.created_at);
}
- async function sortThreads(events: NostrEvent[]): Promise
{
- switch (sortBy) {
+ async function sortThreads(events: NostrEvent[], sortType: 'newest' | 'active' | 'upvoted' = sortBy): Promise {
+ switch (sortType) {
case 'newest':
return sortThreadsSync(events);
case 'active':
// Sort by most recent activity (comments, reactions, or zaps)
// Thread bumping: active threads rise to top
- const activeSorted = await Promise.all(
- events.map(async (event) => {
- const commentRelays = relayManager.getCommentReadRelays();
- const reactionRelays = relayManager.getThreadReadRelays();
- const zapRelays = relayManager.getZapReceiptReadRelays();
-
- // Get most recent comment
- const comments = await nostrClient.fetchEvents(
- [{ kinds: [1111], '#E': [event.id], '#K': ['11'], limit: 1 }],
- commentRelays,
- { useCache: true }
- );
- const lastCommentTime = comments.length > 0
- ? comments.sort((a, b) => b.created_at - a.created_at)[0].created_at
- : 0;
-
- // Get most recent reaction
- const reactions = await nostrClient.fetchEvents(
- [{ kinds: [7], '#e': [event.id], limit: 1 }],
- reactionRelays,
- { useCache: true }
- );
- const lastReactionTime = reactions.length > 0
- ? reactions.sort((a, b) => b.created_at - a.created_at)[0].created_at
- : 0;
-
- // Last activity is the most recent of all activities
- const lastActivity = Math.max(
- event.created_at,
- lastCommentTime,
- lastReactionTime
- );
-
- return { event, lastActivity };
- })
+ // Batch fetch all comments and reactions at once to avoid concurrent request issues
+ const threadIds = events.map(e => e.id);
+ const commentRelays = relayManager.getCommentReadRelays();
+ const reactionRelays = relayManager.getThreadReadRelays();
+
+ // Batch fetch all comments for all threads
+ const allComments = await nostrClient.fetchEvents(
+ [{ kinds: [1111], '#E': threadIds, '#K': ['11'] }],
+ commentRelays,
+ { useCache: true }
);
+
+ // Batch fetch all reactions for all threads
+ const allReactions = await nostrClient.fetchEvents(
+ [{ kinds: [7], '#e': threadIds }],
+ reactionRelays,
+ { useCache: true }
+ );
+
+ // Group comments and reactions by thread ID
+ const commentsByThread = new Map();
+ const reactionsByThread = new Map();
+
+ for (const comment of allComments) {
+ const threadId = comment.tags.find(t => t[0] === 'E' || t[0] === 'e')?.[1];
+ if (threadId) {
+ if (!commentsByThread.has(threadId)) {
+ commentsByThread.set(threadId, []);
+ }
+ commentsByThread.get(threadId)!.push(comment);
+ }
+ }
+
+ for (const reaction of allReactions) {
+ const threadId = reaction.tags.find(t => t[0] === 'e')?.[1];
+ if (threadId) {
+ if (!reactionsByThread.has(threadId)) {
+ reactionsByThread.set(threadId, []);
+ }
+ reactionsByThread.get(threadId)!.push(reaction);
+ }
+ }
+
+ // Calculate last activity for each thread
+ const activeSorted = events.map((event) => {
+ const comments = commentsByThread.get(event.id) || [];
+ const reactions = reactionsByThread.get(event.id) || [];
+
+ const lastCommentTime = comments.length > 0
+ ? Math.max(...comments.map(c => c.created_at))
+ : 0;
+
+ const lastReactionTime = reactions.length > 0
+ ? Math.max(...reactions.map(r => r.created_at))
+ : 0;
+
+ const lastActivity = Math.max(
+ event.created_at,
+ lastCommentTime,
+ lastReactionTime
+ );
+
+ return { event, lastActivity };
+ });
+
return activeSorted
.sort((a, b) => b.lastActivity - a.lastActivity)
.map(({ event }) => event);
case 'upvoted':
// Sort by upvote count
- const upvotedSorted = await Promise.all(
- events.map(async (event) => {
- const config = nostrClient.getConfig();
- const reactionRelays = relayManager.getThreadReadRelays();
- const reactions = await nostrClient.fetchEvents(
- [{ kinds: [7], '#e': [event.id] }],
- reactionRelays,
- { useCache: true }
- );
- const upvoteCount = reactions.filter(
- (r) => r.content.trim() === '+' || r.content.trim() === '⬆️' || r.content.trim() === '↑'
- ).length;
- return { event, upvotes: upvoteCount };
- })
+ // Batch fetch all reactions at once to avoid concurrent request issues
+ const allThreadIds = events.map(e => e.id);
+ const reactionRelaysForUpvotes = relayManager.getThreadReadRelays();
+
+ const allReactionsForUpvotes = await nostrClient.fetchEvents(
+ [{ kinds: [7], '#e': allThreadIds }],
+ reactionRelaysForUpvotes,
+ { useCache: true }
);
+
+ // Group reactions by thread ID
+ const reactionsByThreadForUpvotes = new Map();
+ for (const reaction of allReactionsForUpvotes) {
+ const threadId = reaction.tags.find(t => t[0] === 'e')?.[1];
+ if (threadId) {
+ if (!reactionsByThreadForUpvotes.has(threadId)) {
+ reactionsByThreadForUpvotes.set(threadId, []);
+ }
+ reactionsByThreadForUpvotes.get(threadId)!.push(reaction);
+ }
+ }
+
+ // Calculate upvote count for each thread
+ const upvotedSorted = events.map((event) => {
+ const reactions = reactionsByThreadForUpvotes.get(event.id) || [];
+ const upvoteCount = reactions.filter(
+ (r) => r.content.trim() === '+' || r.content.trim() === '⬆️' || r.content.trim() === '↑'
+ ).length;
+ return { event, upvotes: upvoteCount };
+ });
+
return upvotedSorted
.sort((a, b) => b.upvotes - a.upvotes)
.map(({ event }) => event);
@@ -245,7 +315,7 @@
/>
Show older threads
-
{/each}
@@ -319,7 +389,7 @@
}
}}
>
-
+