feat: list recent supporters
This commit is contained in:
46
src/components/Donation/RecentSupporters.tsx
Normal file
46
src/components/Donation/RecentSupporters.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { cn } from '@/lib/utils'
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import ZapDialog from '../ZapDialog'
|
import ZapDialog from '../ZapDialog'
|
||||||
|
import RecentSupporters from './RecentSupporters'
|
||||||
|
|
||||||
export default function Donation({ className }: { className?: string }) {
|
export default function Donation({ className }: { className?: string }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -38,6 +39,7 @@ export default function Donation({ className }: { className?: string }) {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
<RecentSupporters />
|
||||||
<ZapDialog
|
<ZapDialog
|
||||||
open={open}
|
open={open}
|
||||||
setOpen={setOpen}
|
setOpen={setOpen}
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ export default {
|
|||||||
'Temporarily display this note': 'عرض هذه الملاحظة مؤقتاً',
|
'Temporarily display this note': 'عرض هذه الملاحظة مؤقتاً',
|
||||||
buttonFollowing: 'جارٍ المتابعة',
|
buttonFollowing: 'جارٍ المتابعة',
|
||||||
'Are you sure you want to unfollow this user?':
|
'Are you sure you want to unfollow this user?':
|
||||||
'هل أنت متأكد أنك تريد إلغاء متابعة هذا المستخدم؟'
|
'هل أنت متأكد أنك تريد إلغاء متابعة هذا المستخدم؟',
|
||||||
|
'Recent Supporters': 'الداعمين الجدد'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ export default {
|
|||||||
'Temporarily display this note': 'Notiz vorübergehend anzeigen',
|
'Temporarily display this note': 'Notiz vorübergehend anzeigen',
|
||||||
buttonFollowing: 'Folge',
|
buttonFollowing: 'Folge',
|
||||||
'Are you sure you want to unfollow this user?':
|
'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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ export default {
|
|||||||
'Earlier notifications': 'Earlier notifications',
|
'Earlier notifications': 'Earlier notifications',
|
||||||
'Temporarily display this note': 'Temporarily display this note',
|
'Temporarily display this note': 'Temporarily display this note',
|
||||||
buttonFollowing: 'Following',
|
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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ export default {
|
|||||||
'Temporarily display this note': 'Mostrar esta nota temporalmente',
|
'Temporarily display this note': 'Mostrar esta nota temporalmente',
|
||||||
buttonFollowing: 'Siguiendo',
|
buttonFollowing: 'Siguiendo',
|
||||||
'Are you sure you want to unfollow this user?':
|
'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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ export default {
|
|||||||
'Temporarily display this note': 'Afficher temporairement cette note',
|
'Temporarily display this note': 'Afficher temporairement cette note',
|
||||||
buttonFollowing: 'Suivi',
|
buttonFollowing: 'Suivi',
|
||||||
'Are you sure you want to unfollow this user?':
|
'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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ export default {
|
|||||||
'Earlier notifications': '以前の通知',
|
'Earlier notifications': '以前の通知',
|
||||||
'Temporarily display this note': 'このノートを一時的に表示',
|
'Temporarily display this note': 'このノートを一時的に表示',
|
||||||
buttonFollowing: 'フォロー中',
|
buttonFollowing: 'フォロー中',
|
||||||
'Are you sure you want to unfollow this user?': 'このユーザーのフォローを解除しますか?'
|
'Are you sure you want to unfollow this user?': 'このユーザーのフォローを解除しますか?',
|
||||||
|
'Recent Supporters': '最近のサポーター'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ export default {
|
|||||||
'Temporarily display this note': 'Tymczas wyświetl ten wpis',
|
'Temporarily display this note': 'Tymczas wyświetl ten wpis',
|
||||||
buttonFollowing: 'Obserwujesz',
|
buttonFollowing: 'Obserwujesz',
|
||||||
'Are you sure you want to unfollow this user?':
|
'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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
translation: {
|
translation: {
|
||||||
// NOTE: The translations below were generated by ChatGPT and have not yet been verified.
|
|
||||||
'Welcome! 🥳': 'Bem-vindo! 🥳',
|
'Welcome! 🥳': 'Bem-vindo! 🥳',
|
||||||
About: 'Sobre',
|
About: 'Sobre',
|
||||||
'New Note': 'Nova nota',
|
'New Note': 'Nova nota',
|
||||||
@@ -207,6 +206,9 @@ export default {
|
|||||||
'Temporarily display this note': 'Exibir esta nota temporariamente',
|
'Temporarily display this note': 'Exibir esta nota temporariamente',
|
||||||
buttonFollowing: 'Seguindo',
|
buttonFollowing: 'Seguindo',
|
||||||
'Are you sure you want to unfollow this user?':
|
'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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ export default {
|
|||||||
'Temporarily display this note': 'Exibir esta nota temporariamente',
|
'Temporarily display this note': 'Exibir esta nota temporariamente',
|
||||||
buttonFollowing: 'Seguindo',
|
buttonFollowing: 'Seguindo',
|
||||||
'Are you sure you want to unfollow this user?':
|
'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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ export default {
|
|||||||
'Temporarily display this note': 'Временно отобразить эту заметку',
|
'Temporarily display this note': 'Временно отобразить эту заметку',
|
||||||
buttonFollowing: 'Подписан',
|
buttonFollowing: 'Подписан',
|
||||||
'Are you sure you want to unfollow this user?':
|
'Are you sure you want to unfollow this user?':
|
||||||
'Вы уверены, что хотите отписаться от этого пользователя?'
|
'Вы уверены, что хотите отписаться от этого пользователя?',
|
||||||
|
'Recent Supporters': 'Недавние спонсоры'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ export default {
|
|||||||
'Earlier notifications': '更早的通知',
|
'Earlier notifications': '更早的通知',
|
||||||
'Temporarily display this note': '临时显示此笔记',
|
'Temporarily display this note': '临时显示此笔记',
|
||||||
buttonFollowing: '已关注',
|
buttonFollowing: '已关注',
|
||||||
'Are you sure you want to unfollow this user?': '确定要取消关注此用户吗?'
|
'Are you sure you want to unfollow this user?': '确定要取消关注此用户吗?',
|
||||||
|
'Recent Supporters': '最近的支持者'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BIG_RELAY_URLS } from '@/constants'
|
import { BIG_RELAY_URLS, CODY_PUBKEY } from '@/constants'
|
||||||
import { extractZapInfoFromReceipt } from '@/lib/event'
|
import { extractZapInfoFromReceipt } from '@/lib/event'
|
||||||
import { TProfile } from '@/types'
|
import { TProfile } from '@/types'
|
||||||
import {
|
import {
|
||||||
@@ -17,9 +17,12 @@ import { makeZapRequest } from 'nostr-tools/nip57'
|
|||||||
import { utf8Decoder } from 'nostr-tools/utils'
|
import { utf8Decoder } from 'nostr-tools/utils'
|
||||||
import client from './client.service'
|
import client from './client.service'
|
||||||
|
|
||||||
|
export type TRecentSupporter = { pubkey: string; amount: number; comment?: string }
|
||||||
|
|
||||||
class LightningService {
|
class LightningService {
|
||||||
static instance: LightningService
|
static instance: LightningService
|
||||||
private provider: WebLNProvider | null = null
|
private provider: WebLNProvider | null = null
|
||||||
|
private recentSupportersCache: TRecentSupporter[] | null = null
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!LightningService.instance) {
|
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 | {
|
private async getZapEndpoint(profile: TProfile): Promise<null | {
|
||||||
callback: string
|
callback: string
|
||||||
lnurl: string
|
lnurl: string
|
||||||
|
|||||||
Reference in New Issue
Block a user