From c6f42fb20b739961555f7028443d89e9bc7cc56f Mon Sep 17 00:00:00 2001 From: Silberengel Date: Mon, 2 Feb 2026 16:05:57 +0100 Subject: [PATCH] more features --- src/lib/modules/comments/CommentForm.svelte | 2 +- src/lib/modules/feed/CreateKind1Form.svelte | 2 +- src/lib/modules/feed/Kind1FeedPage.svelte | 4 +- src/lib/modules/feed/ReplyToKind1Form.svelte | 2 +- .../modules/threads/CreateThreadForm.svelte | 2 +- src/lib/modules/threads/ThreadCard.svelte | 166 ++++++++++++------ src/lib/modules/threads/ThreadList.svelte | 4 +- src/lib/modules/zaps/ZapInvoiceModal.svelte | 2 +- src/lib/modules/zaps/ZapReceipt.svelte | 32 +++- src/routes/+page.svelte | 2 +- src/routes/login/+page.svelte | 2 +- 11 files changed, 146 insertions(+), 74 deletions(-) diff --git a/src/lib/modules/comments/CommentForm.svelte b/src/lib/modules/comments/CommentForm.svelte index b6718ae..899545a 100644 --- a/src/lib/modules/comments/CommentForm.svelte +++ b/src/lib/modules/comments/CommentForm.svelte @@ -106,7 +106,7 @@ {/if} @@ -234,7 +234,7 @@
diff --git a/src/lib/modules/feed/ReplyToKind1Form.svelte b/src/lib/modules/feed/ReplyToKind1Form.svelte index 1999886..98b2b1d 100644 --- a/src/lib/modules/feed/ReplyToKind1Form.svelte +++ b/src/lib/modules/feed/ReplyToKind1Form.svelte @@ -115,7 +115,7 @@ {/if}
- diff --git a/src/lib/modules/threads/ThreadCard.svelte b/src/lib/modules/threads/ThreadCard.svelte index 93d269d..b3021bf 100644 --- a/src/lib/modules/threads/ThreadCard.svelte +++ b/src/lib/modules/threads/ThreadCard.svelte @@ -16,6 +16,7 @@ let commentCount = $state(0); let zapTotal = $state(0); let zapCount = $state(0); + let latestResponseTime = $state(null); let loadingStats = $state(true); onMount(async () => { @@ -24,60 +25,96 @@ async function loadStats() { loadingStats = true; + const timeout = 30000; // 30 seconds + try { const config = nostrClient.getConfig(); - // Load reactions (kind 7) - const reactionRelays = relayManager.getThreadReadRelays(); - const reactionEvents = await nostrClient.fetchEvents( - [{ kinds: [7], '#e': [thread.id] }], - reactionRelays, - { useCache: true } - ); - - // Count upvotes (+) and downvotes (-) - for (const reaction of reactionEvents) { - const content = reaction.content.trim(); - if (content === '+' || content === '⬆️' || content === '↑') { - upvotes++; - } else if (content === '-' || content === '⬇️' || content === '↓') { - downvotes++; - } - } - - // Load comments (kind 1111) - const commentRelays = relayManager.getCommentReadRelays(); - const commentEvents = await nostrClient.fetchEvents( - [{ kinds: [1111], '#E': [thread.id], '#K': ['11'] }], - commentRelays, - { useCache: true } - ); - commentCount = commentEvents.length; - - // Load zap receipts (kind 9735) - const zapRelays = relayManager.getZapReceiptReadRelays(); - const zapReceipts = await nostrClient.fetchEvents( - [{ kinds: [9735], '#e': [thread.id] }], - zapRelays, - { useCache: true } - ); + // Create a timeout promise + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Stats loading timeout')), timeout); + }); - // Calculate zap totals - const threshold = config.zapThreshold; - zapCount = 0; - zapTotal = 0; - for (const receipt of zapReceipts) { - const amountTag = receipt.tags.find((t) => t[0] === 'amount'); - if (amountTag && amountTag[1]) { - const amount = parseInt(amountTag[1], 10); - if (!isNaN(amount) && amount >= threshold) { - zapTotal += amount; - zapCount++; + // Race between loading and timeout + await Promise.race([ + (async () => { + // Load reactions (kind 7) + const reactionRelays = relayManager.getThreadReadRelays(); + const reactionEvents = await nostrClient.fetchEvents( + [{ kinds: [7], '#e': [thread.id] }], + reactionRelays, + { useCache: true } + ); + + // Count upvotes (+) and downvotes (-) + for (const reaction of reactionEvents) { + const content = reaction.content.trim(); + if (content === '+' || content === '⬆️' || content === '↑') { + upvotes++; + } else if (content === '-' || content === '⬇️' || content === '↓') { + downvotes++; + } + } + + // Load comments (kind 1111) + const commentRelays = relayManager.getCommentReadRelays(); + const commentEvents = await nostrClient.fetchEvents( + [{ kinds: [1111], '#E': [thread.id], '#K': ['11'] }], + commentRelays, + { useCache: true } + ); + commentCount = commentEvents.length; + + // Load zap receipts (kind 9735) + const zapRelays = relayManager.getZapReceiptReadRelays(); + const zapReceipts = await nostrClient.fetchEvents( + [{ kinds: [9735], '#e': [thread.id] }], + zapRelays, + { useCache: true } + ); + + // Calculate zap totals + const threshold = config.zapThreshold; + zapCount = 0; + zapTotal = 0; + for (const receipt of zapReceipts) { + const amountTag = receipt.tags.find((t) => t[0] === 'amount'); + if (amountTag && amountTag[1]) { + const amount = parseInt(amountTag[1], 10); + if (!isNaN(amount) && amount >= threshold) { + zapTotal += amount; + zapCount++; + } + } + } + + // Find latest response time (most recent comment, reaction, or zap) + let latestTime = thread.created_at; + if (commentEvents.length > 0) { + const latestComment = commentEvents.sort((a, b) => b.created_at - a.created_at)[0]; + latestTime = Math.max(latestTime, latestComment.created_at); } - } - } + if (reactionEvents.length > 0) { + const latestReaction = reactionEvents.sort((a, b) => b.created_at - a.created_at)[0]; + latestTime = Math.max(latestTime, latestReaction.created_at); + } + if (zapReceipts.length > 0) { + const latestZap = zapReceipts.sort((a, b) => b.created_at - a.created_at)[0]; + latestTime = Math.max(latestTime, latestZap.created_at); + } + latestResponseTime = latestTime > thread.created_at ? latestTime : null; + })(), + timeoutPromise + ]); } catch (error) { console.error('Error loading thread stats:', error); + // On timeout or error, show zero stats instead of loading forever + upvotes = 0; + downvotes = 0; + commentCount = 0; + zapTotal = 0; + zapCount = 0; + latestResponseTime = null; } finally { loadingStats = false; } @@ -109,6 +146,20 @@ return 'just now'; } + function getLatestResponseTime(): string { + if (!latestResponseTime) return ''; + const now = Math.floor(Date.now() / 1000); + const diff = now - latestResponseTime; + const hours = Math.floor(diff / 3600); + const days = Math.floor(diff / 86400); + const minutes = Math.floor(diff / 60); + + if (days > 0) return `${days}d ago`; + if (hours > 0) return `${hours}h ago`; + if (minutes > 0) return `${minutes}m ago`; + return 'just now'; + } + function getClientName(): string | null { const clientTag = thread.tags.find((t) => t[0] === 'client'); return clientTag?.[1] || null; @@ -140,18 +191,23 @@ {/if} -
-
- {#if !loadingStats} - ↑ {upvotes} - ↓ {downvotes} - {commentCount} {commentCount === 1 ? 'comment' : 'comments'} +
+
+ {#if loadingStats} + Loading stats... + {:else} + ↑ {upvotes} + ↓ {downvotes} + {commentCount} {commentCount === 1 ? 'comment' : 'comments'} + {#if latestResponseTime} + Last: {getLatestResponseTime()} + {/if} {#if zapCount > 0} - ⚡ {zapTotal.toLocaleString()} sats ({zapCount}) + ⚡ {zapTotal.toLocaleString()} sats ({zapCount}) {/if} {/if}
- View thread → + View thread →
diff --git a/src/lib/modules/threads/ThreadList.svelte b/src/lib/modules/threads/ThreadList.svelte index 8110ef1..06dad25 100644 --- a/src/lib/modules/threads/ThreadList.svelte +++ b/src/lib/modules/threads/ThreadList.svelte @@ -251,7 +251,7 @@ diff --git a/src/lib/modules/zaps/ZapReceipt.svelte b/src/lib/modules/zaps/ZapReceipt.svelte index a8c96c4..2c4cf63 100644 --- a/src/lib/modules/zaps/ZapReceipt.svelte +++ b/src/lib/modules/zaps/ZapReceipt.svelte @@ -21,12 +21,19 @@ async function loadZapReceipts() { loading = true; + const timeout = 30000; // 30 seconds + try { const config = nostrClient.getConfig(); const threshold = config.zapThreshold; + // Create a timeout promise + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Zap loading timeout')), timeout); + }); + // Fetch zap receipts (kind 9735) for this event - const filters = [ + const filters: any[] = [ { kinds: [9735], '#e': [eventId] @@ -37,17 +44,24 @@ filters[0]['#p'] = [pubkey]; } - const receipts = await nostrClient.fetchEvents( - filters, - [...config.defaultRelays], - { useCache: true, cacheResults: true, onUpdate: (updated) => { - processReceipts(updated); - }} - ); + // Race between loading and timeout + const receipts = await Promise.race([ + nostrClient.fetchEvents( + filters, + [...config.defaultRelays], + { useCache: true, cacheResults: true, onUpdate: (updated) => { + processReceipts(updated); + }} + ), + timeoutPromise + ]); processReceipts(receipts); } catch (error) { console.error('Error loading zap receipts:', error); + // On timeout or error, show empty state + zapReceipts = []; + totalAmount = 0; } finally { loading = false; } @@ -101,6 +115,8 @@ ({zapReceipts.length} {zapReceipts.length === 1 ? 'zap' : 'zaps'})
+ {:else} + No zaps {/if}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 9e3ff37..1f821c8 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -34,7 +34,7 @@ {#if isLoggedIn} diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index 68b8257..2d9235d 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -45,7 +45,7 @@