feat: list recent supporters

This commit is contained in:
codytseng
2025-03-03 20:08:34 +08:00
parent 55bd996970
commit 94f35be93e
14 changed files with 106 additions and 13 deletions

View File

@@ -0,0 +1,46 @@
import { formatAmount } from '@/lib/lightning'
import lightning, { TRecentSupporter } from '@/services/lightning.service'
import { useEffect, useState } from 'react'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
import { useTranslation } from 'react-i18next'
export default function RecentSupporters() {
const { t } = useTranslation()
const [supporters, setSupporters] = useState<TRecentSupporter[]>([])
useEffect(() => {
const init = async () => {
const items = await lightning.fetchRecentSupporters()
setSupporters(items)
}
init()
}, [])
if (!supporters.length) return null
return (
<div className="space-y-2">
<div className="font-semibold text-center">{t('Recent Supporters')}</div>
<div className="flex flex-col gap-2">
{supporters.map((item, index) => (
<div
key={index}
className="flex items-center justify-between rounded-md border p-2 sm:p-4 gap-2"
>
<div className="flex items-center gap-2 flex-1 w-0">
<UserAvatar userId={item.pubkey} />
<div className="flex-1 w-0">
<Username className="font-semibold w-fit" userId={item.pubkey} />
<div className="text-xs text-muted-foreground line-clamp-3">{item.comment}</div>
</div>
</div>
<div className="font-semibold text-yellow-400 shrink-0">
{formatAmount(item.amount)} {t('sats')}
</div>
</div>
))}
</div>
</div>
)
}

View File

@@ -4,6 +4,7 @@ import { cn } from '@/lib/utils'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ZapDialog from '../ZapDialog'
import RecentSupporters from './RecentSupporters'
export default function Donation({ className }: { className?: string }) {
const { t } = useTranslation()
@@ -38,6 +39,7 @@ export default function Donation({ className }: { className?: string }) {
)
})}
</div>
<RecentSupporters />
<ZapDialog
open={open}
setOpen={setOpen}

View File

@@ -204,6 +204,7 @@ export default {
'Temporarily display this note': 'عرض هذه الملاحظة مؤقتاً',
buttonFollowing: 'جارٍ المتابعة',
'Are you sure you want to unfollow this user?':
'هل أنت متأكد أنك تريد إلغاء متابعة هذا المستخدم؟'
'هل أنت متأكد أنك تريد إلغاء متابعة هذا المستخدم؟',
'Recent Supporters': 'الداعمين الجدد'
}
}

View File

@@ -209,6 +209,7 @@ export default {
'Temporarily display this note': 'Notiz vorübergehend anzeigen',
buttonFollowing: 'Folge',
'Are you sure you want to unfollow this user?':
'Möchtest du diesem Benutzer wirklich nicht mehr folgen?'
'Möchtest du diesem Benutzer wirklich nicht mehr folgen?',
'Recent Supporters': 'Neueste Unterstützer'
}
}

View File

@@ -204,6 +204,7 @@ export default {
'Earlier notifications': 'Earlier notifications',
'Temporarily display this note': 'Temporarily display this note',
buttonFollowing: 'Following',
'Are you sure you want to unfollow this user?': 'Are you sure you want to unfollow this user?'
'Are you sure you want to unfollow this user?': 'Are you sure you want to unfollow this user?',
'Recent Supporters': 'Recent Supporters'
}
}

View File

@@ -209,6 +209,7 @@ export default {
'Temporarily display this note': 'Mostrar esta nota temporalmente',
buttonFollowing: 'Siguiendo',
'Are you sure you want to unfollow this user?':
'¿Estás seguro de que deseas dejar de seguir a este usuario?'
'¿Estás seguro de que deseas dejar de seguir a este usuario?',
'Recent Supporters': 'Últimos patrocinadores'
}
}

View File

@@ -207,6 +207,7 @@ export default {
'Temporarily display this note': 'Afficher temporairement cette note',
buttonFollowing: 'Suivi',
'Are you sure you want to unfollow this user?':
'Êtes-vous sûr de vouloir arrêter de suivre cet utilisateur ?'
'Êtes-vous sûr de vouloir arrêter de suivre cet utilisateur ?',
'Recent Supporters': 'Derniers soutiens'
}
}

View File

@@ -205,6 +205,7 @@ export default {
'Earlier notifications': '以前の通知',
'Temporarily display this note': 'このノートを一時的に表示',
buttonFollowing: 'フォロー中',
'Are you sure you want to unfollow this user?': 'このユーザーのフォローを解除しますか?'
'Are you sure you want to unfollow this user?': 'このユーザーのフォローを解除しますか?',
'Recent Supporters': '最近のサポーター'
}
}

View File

@@ -207,6 +207,7 @@ export default {
'Temporarily display this note': 'Tymczas wyświetl ten wpis',
buttonFollowing: 'Obserwujesz',
'Are you sure you want to unfollow this user?':
'Czy na pewno chcesz przestać obserwować tego użytkownika?'
'Czy na pewno chcesz przestać obserwować tego użytkownika?',
'Recent Supporters': 'Ostatni wspierający'
}
}

View File

@@ -1,6 +1,5 @@
export default {
translation: {
// NOTE: The translations below were generated by ChatGPT and have not yet been verified.
'Welcome! 🥳': 'Bem-vindo! 🥳',
About: 'Sobre',
'New Note': 'Nova nota',
@@ -207,6 +206,9 @@ export default {
'Temporarily display this note': 'Exibir esta nota temporariamente',
buttonFollowing: 'Seguindo',
'Are you sure you want to unfollow this user?':
'Tem certeza de que deseja deixar de seguir este usuário?'
'Tem certeza de que deseja deixar de seguir este usuário?',
// NOTE: The translations below were generated by ChatGPT and have not yet been verified.
'Recent Supporters': 'Apoiadores recentes'
}
}

View File

@@ -207,6 +207,7 @@ export default {
'Temporarily display this note': 'Exibir esta nota temporariamente',
buttonFollowing: 'Seguindo',
'Are you sure you want to unfollow this user?':
'Tem certeza de que deseja deixar de seguir este usuário?'
'Tem certeza de que deseja deixar de seguir este usuário?',
'Recent Supporters': 'Apoiadores Recentes'
}
}

View File

@@ -209,6 +209,7 @@ export default {
'Temporarily display this note': 'Временно отобразить эту заметку',
buttonFollowing: 'Подписан',
'Are you sure you want to unfollow this user?':
'Вы уверены, что хотите отписаться от этого пользователя?'
'Вы уверены, что хотите отписаться от этого пользователя?',
'Recent Supporters': 'Недавние спонсоры'
}
}

View File

@@ -205,6 +205,7 @@ export default {
'Earlier notifications': '更早的通知',
'Temporarily display this note': '临时显示此笔记',
buttonFollowing: '已关注',
'Are you sure you want to unfollow this user?': '确定要取消关注此用户吗?'
'Are you sure you want to unfollow this user?': '确定要取消关注此用户吗?',
'Recent Supporters': '最近的支持者'
}
}

View File

@@ -1,4 +1,4 @@
import { BIG_RELAY_URLS } from '@/constants'
import { BIG_RELAY_URLS, CODY_PUBKEY } from '@/constants'
import { extractZapInfoFromReceipt } from '@/lib/event'
import { TProfile } from '@/types'
import {
@@ -17,9 +17,12 @@ import { makeZapRequest } from 'nostr-tools/nip57'
import { utf8Decoder } from 'nostr-tools/utils'
import client from './client.service'
export type TRecentSupporter = { pubkey: string; amount: number; comment?: string }
class LightningService {
static instance: LightningService
private provider: WebLNProvider | null = null
private recentSupportersCache: TRecentSupporter[] | null = null
constructor() {
if (!LightningService.instance) {
@@ -150,6 +153,36 @@ class LightningService {
})
}
async fetchRecentSupporters() {
if (this.recentSupportersCache) {
return this.recentSupportersCache
}
const relayList = await client.fetchRelayList(CODY_PUBKEY)
const events = await client.fetchEvents(relayList.read.slice(0, 4), {
authors: ['79f00d3f5a19ec806189fcab03c1be4ff81d18ee4f653c88fac41fe03570f432'], // alby
kinds: [kinds.Zap],
'#p': [CODY_PUBKEY],
since: dayjs().subtract(1, 'month').unix()
})
events.sort((a, b) => b.created_at - a.created_at)
const map = new Map<string, { pubkey: string; amount: number; comment?: string }>()
events.forEach((event) => {
const info = extractZapInfoFromReceipt(event)
if (!info || info.eventId || !info.senderPubkey || info.senderPubkey === CODY_PUBKEY) return
const { amount, comment, senderPubkey } = info
const item = map.get(senderPubkey)
if (!item) {
map.set(senderPubkey, { pubkey: senderPubkey, amount, comment })
} else {
item.amount += amount
if (!item.comment && comment) item.comment = comment
}
})
this.recentSupportersCache = Array.from(map.values()).sort((a, b) => b.amount - a.amount)
return this.recentSupportersCache
}
private async getZapEndpoint(profile: TProfile): Promise<null | {
callback: string
lnurl: string