You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
130 lines
3.6 KiB
130 lines
3.6 KiB
<script lang="ts"> |
|
import { nostrClient } from '../../services/nostr/nostr-client.js'; |
|
import { onMount } from 'svelte'; |
|
import type { NostrEvent } from '../../types/nostr.js'; |
|
|
|
interface Props { |
|
eventId: string; // The event that was zapped |
|
pubkey?: string; // Optional: filter by zapped pubkey |
|
} |
|
|
|
let { eventId, pubkey }: Props = $props(); |
|
|
|
let zapReceipts = $state<NostrEvent[]>([]); |
|
let totalAmount = $state<number>(0); |
|
let loading = $state(true); |
|
|
|
onMount(async () => { |
|
await nostrClient.initialize(); |
|
loadZapReceipts(); |
|
}); |
|
|
|
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<never>((_, reject) => { |
|
setTimeout(() => reject(new Error('Zap loading timeout')), timeout); |
|
}); |
|
|
|
// Fetch zap receipts (kind 9735) for this event |
|
const filters: any[] = [ |
|
{ |
|
kinds: [9735], |
|
'#e': [eventId] |
|
} |
|
]; |
|
|
|
if (pubkey) { |
|
filters[0]['#p'] = [pubkey]; |
|
} |
|
|
|
// 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; |
|
} |
|
} |
|
|
|
function processReceipts(receipts: NostrEvent[]) { |
|
const config = nostrClient.getConfig(); |
|
const threshold = config.zapThreshold; |
|
|
|
// Filter by threshold and extract amounts |
|
const validReceipts = receipts.filter((receipt) => { |
|
const amountTag = receipt.tags.find((t) => t[0] === 'amount'); |
|
if (amountTag && amountTag[1]) { |
|
const amount = parseInt(amountTag[1], 10); |
|
return !isNaN(amount) && amount >= threshold; |
|
} |
|
return false; |
|
}); |
|
|
|
zapReceipts = validReceipts; |
|
|
|
// Calculate total |
|
totalAmount = validReceipts.reduce((sum, receipt) => { |
|
const amountTag = receipt.tags.find((t) => t[0] === 'amount'); |
|
if (amountTag && amountTag[1]) { |
|
const amount = parseInt(amountTag[1], 10); |
|
return sum + (isNaN(amount) ? 0 : amount); |
|
} |
|
return sum; |
|
}, 0); |
|
} |
|
|
|
function getAmount(receipt: NostrEvent): number { |
|
const amountTag = receipt.tags.find((t) => t[0] === 'amount'); |
|
if (amountTag && amountTag[1]) { |
|
const amount = parseInt(amountTag[1], 10); |
|
return isNaN(amount) ? 0 : amount; |
|
} |
|
return 0; |
|
} |
|
</script> |
|
|
|
<div class="zap-receipts"> |
|
{#if loading} |
|
<span class="text-sm text-fog-text-light dark:text-fog-dark-text-light">Loading zaps...</span> |
|
{:else if zapReceipts.length > 0} |
|
<div class="flex items-center gap-2"> |
|
<span class="text-lg">⚡</span> |
|
<span class="text-sm font-semibold">{totalAmount.toLocaleString()} sats</span> |
|
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light"> |
|
({zapReceipts.length} {zapReceipts.length === 1 ? 'zap' : 'zaps'}) |
|
</span> |
|
</div> |
|
{:else} |
|
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">No zaps</span> |
|
{/if} |
|
</div> |
|
|
|
<style> |
|
.zap-receipts { |
|
display: flex; |
|
align-items: center; |
|
display: inline-flex; |
|
align-items: center; |
|
} |
|
</style>
|
|
|