|
|
|
@ -16,6 +16,7 @@ |
|
|
|
let commentCount = $state(0); |
|
|
|
let commentCount = $state(0); |
|
|
|
let zapTotal = $state(0); |
|
|
|
let zapTotal = $state(0); |
|
|
|
let zapCount = $state(0); |
|
|
|
let zapCount = $state(0); |
|
|
|
|
|
|
|
let latestResponseTime = $state<number | null>(null); |
|
|
|
let loadingStats = $state(true); |
|
|
|
let loadingStats = $state(true); |
|
|
|
|
|
|
|
|
|
|
|
onMount(async () => { |
|
|
|
onMount(async () => { |
|
|
|
@ -24,60 +25,96 @@ |
|
|
|
|
|
|
|
|
|
|
|
async function loadStats() { |
|
|
|
async function loadStats() { |
|
|
|
loadingStats = true; |
|
|
|
loadingStats = true; |
|
|
|
|
|
|
|
const timeout = 30000; // 30 seconds |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
try { |
|
|
|
const config = nostrClient.getConfig(); |
|
|
|
const config = nostrClient.getConfig(); |
|
|
|
|
|
|
|
|
|
|
|
// Load reactions (kind 7) |
|
|
|
// Create a timeout promise |
|
|
|
const reactionRelays = relayManager.getThreadReadRelays(); |
|
|
|
const timeoutPromise = new Promise<never>((_, reject) => { |
|
|
|
const reactionEvents = await nostrClient.fetchEvents( |
|
|
|
setTimeout(() => reject(new Error('Stats loading timeout')), timeout); |
|
|
|
[{ 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 |
|
|
|
// Race between loading and timeout |
|
|
|
const threshold = config.zapThreshold; |
|
|
|
await Promise.race([ |
|
|
|
zapCount = 0; |
|
|
|
(async () => { |
|
|
|
zapTotal = 0; |
|
|
|
// Load reactions (kind 7) |
|
|
|
for (const receipt of zapReceipts) { |
|
|
|
const reactionRelays = relayManager.getThreadReadRelays(); |
|
|
|
const amountTag = receipt.tags.find((t) => t[0] === 'amount'); |
|
|
|
const reactionEvents = await nostrClient.fetchEvents( |
|
|
|
if (amountTag && amountTag[1]) { |
|
|
|
[{ kinds: [7], '#e': [thread.id] }], |
|
|
|
const amount = parseInt(amountTag[1], 10); |
|
|
|
reactionRelays, |
|
|
|
if (!isNaN(amount) && amount >= threshold) { |
|
|
|
{ useCache: true } |
|
|
|
zapTotal += amount; |
|
|
|
); |
|
|
|
zapCount++; |
|
|
|
|
|
|
|
|
|
|
|
// 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) { |
|
|
|
} catch (error) { |
|
|
|
console.error('Error loading thread stats:', 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 { |
|
|
|
} finally { |
|
|
|
loadingStats = false; |
|
|
|
loadingStats = false; |
|
|
|
} |
|
|
|
} |
|
|
|
@ -109,6 +146,20 @@ |
|
|
|
return 'just now'; |
|
|
|
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 { |
|
|
|
function getClientName(): string | null { |
|
|
|
const clientTag = thread.tags.find((t) => t[0] === 'client'); |
|
|
|
const clientTag = thread.tags.find((t) => t[0] === 'client'); |
|
|
|
return clientTag?.[1] || null; |
|
|
|
return clientTag?.[1] || null; |
|
|
|
@ -140,18 +191,23 @@ |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
|
|
|
|
|
|
|
|
<div class="flex items-center justify-between text-xs text-fog-text-light dark:text-fog-dark-text-light"> |
|
|
|
<div class="flex items-center justify-between text-xs text-fog-text dark:text-fog-dark-text"> |
|
|
|
<div class="flex items-center gap-4"> |
|
|
|
<div class="flex items-center gap-4 flex-wrap"> |
|
|
|
{#if !loadingStats} |
|
|
|
{#if loadingStats} |
|
|
|
<span>↑ {upvotes}</span> |
|
|
|
<span class="text-fog-text-light dark:text-fog-dark-text-light">Loading stats...</span> |
|
|
|
<span>↓ {downvotes}</span> |
|
|
|
{:else} |
|
|
|
<span>{commentCount} {commentCount === 1 ? 'comment' : 'comments'}</span> |
|
|
|
<span class="font-medium">↑ {upvotes}</span> |
|
|
|
|
|
|
|
<span class="font-medium">↓ {downvotes}</span> |
|
|
|
|
|
|
|
<span class="font-medium">{commentCount} {commentCount === 1 ? 'comment' : 'comments'}</span> |
|
|
|
|
|
|
|
{#if latestResponseTime} |
|
|
|
|
|
|
|
<span class="text-fog-text-light dark:text-fog-dark-text-light">Last: {getLatestResponseTime()}</span> |
|
|
|
|
|
|
|
{/if} |
|
|
|
{#if zapCount > 0} |
|
|
|
{#if zapCount > 0} |
|
|
|
<span>⚡ {zapTotal.toLocaleString()} sats ({zapCount})</span> |
|
|
|
<span class="font-medium">⚡ {zapTotal.toLocaleString()} sats ({zapCount})</span> |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<a href="/thread/{thread.id}">View thread →</a> |
|
|
|
<a href="/thread/{thread.id}" class="ml-2 text-fog-accent dark:text-fog-dark-accent hover:underline">View thread →</a> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</article> |
|
|
|
</article> |
|
|
|
|
|
|
|
|
|
|
|
|