feat: zap details (#731)

Co-authored-by: The Daniel <dmnyc@users.noreply.github.com>
Co-authored-by: The Daniel ️ <dmnycnet@proton.me>
This commit is contained in:
Cody Tseng 2026-01-13 22:54:50 +08:00 committed by GitHub
parent 603bd35b4a
commit 7e8f1692ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 138 additions and 47 deletions

View file

@ -1,12 +1,14 @@
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
import { useStuff } from '@/hooks/useStuff' import { useStuff } from '@/hooks/useStuff'
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
import { createFakeEvent } from '@/lib/event'
import { formatAmount } from '@/lib/lightning' import { formatAmount } from '@/lib/lightning'
import { Zap } from 'lucide-react' import { Zap } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import ContentPreview from '../ContentPreview'
import { SimpleUserAvatar } from '../UserAvatar' import { SimpleUserAvatar } from '../UserAvatar'
import ZapDialog from '../ZapDialog' import ZapDetailDialog from '../ZapDetailDialog'
export default function TopZaps({ stuff }: { stuff: Event | string }) { export default function TopZaps({ stuff }: { stuff: Event | string }) {
const { event, stuffKey } = useStuff(stuff) const { event, stuffKey } = useStuff(stuff)
@ -22,34 +24,35 @@ export default function TopZaps({ stuff }: { stuff: Event | string }) {
<ScrollArea className="pb-2 mb-1"> <ScrollArea className="pb-2 mb-1">
<div className="flex gap-1"> <div className="flex gap-1">
{topZaps.map((zap, index) => ( {topZaps.map((zap, index) => (
<div <div key={zap.pr}>
key={zap.pr} <div
className="flex gap-1 py-1 pl-1 pr-2 text-sm max-w-72 rounded-full bg-muted/80 items-center text-yellow-400 border border-yellow-400 hover:bg-yellow-400/20 cursor-pointer" className="flex gap-1 py-1 pl-1 pr-2 text-sm max-w-72 rounded-full bg-muted/80 items-center text-yellow-400 border border-yellow-400 hover:bg-yellow-400/20 cursor-pointer"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
setZapIndex(index) setZapIndex(index)
}} }}
> >
<SimpleUserAvatar userId={zap.pubkey} size="xSmall" /> <SimpleUserAvatar userId={zap.pubkey} size="xSmall" />
<Zap className="size-3 fill-yellow-400 shrink-0" /> <Zap className="size-3 fill-yellow-400 shrink-0" />
<div className="font-semibold">{formatAmount(zap.amount)}</div> <div className="font-semibold">{formatAmount(zap.amount)}</div>
<div className="truncate">{zap.comment}</div> <ContentPreview
<div onClick={(e) => e.stopPropagation()}> className="truncate"
<ZapDialog event={createFakeEvent({
open={zapIndex === index} content: zap.comment
setOpen={(open) => { })}
if (open) {
setZapIndex(index)
} else {
setZapIndex(-1)
}
}}
pubkey={event.pubkey}
event={event}
defaultAmount={zap.amount}
defaultComment={zap.comment}
/> />
</div> </div>
<ZapDetailDialog
open={zapIndex === index}
setOpen={(open) => {
if (open) {
setZapIndex(index)
} else {
setZapIndex(-1)
}
}}
zap={zap}
/>
</div> </div>
))} ))}
</div> </div>

View file

@ -0,0 +1,70 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'
import { formatAmount } from '@/lib/lightning'
import { Zap } from 'lucide-react'
import { Dispatch, SetStateAction } from 'react'
import { useTranslation } from 'react-i18next'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
import Content from '../Content'
import { FormattedTimestamp } from '../FormattedTimestamp'
interface ZapDetailDialogProps {
open: boolean
setOpen: Dispatch<SetStateAction<boolean>>
zap: {
pubkey: string
amount: number
comment?: string
created_at: number
}
}
export default function ZapDetailDialog({ open, setOpen, zap }: ZapDetailDialogProps) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const content = (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<UserAvatar userId={zap.pubkey} size="medium" />
<div className="flex-1">
<Username userId={zap.pubkey} className="font-semibold" />
<div className="flex items-center gap-1 text-sm text-muted-foreground">
<FormattedTimestamp timestamp={zap.created_at} />
</div>
</div>
<div className="flex items-center gap-1 text-yellow-400">
<Zap className="size-5 fill-yellow-400" />
<span className="text-lg font-bold">{formatAmount(zap.amount)}</span>
</div>
</div>
{zap.comment && <Content content={zap.comment} />}
</div>
)
if (isSmallScreen) {
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerContent className="px-4 pb-4">
<DrawerHeader>
<DrawerTitle>{t('Zap Details')}</DrawerTitle>
</DrawerHeader>
{content}
</DrawerContent>
</Drawer>
)
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent onClick={(e) => e.stopPropagation()}>
<DialogHeader>
<DialogTitle>{t('Zap Details')}</DialogTitle>
</DialogHeader>
{content}
</DialogContent>
</Dialog>
)
}

View file

@ -662,6 +662,7 @@ export default {
'تحذير: يرجى عدم تعديل هذه الإعدادات بشكل عشوائي، فقد يؤثر ذلك على تجربتك الأساسية.', 'تحذير: يرجى عدم تعديل هذه الإعدادات بشكل عشوائي، فقد يؤثر ذلك على تجربتك الأساسية.',
'Invalid relay URL': 'عنوان URL للمرحل غير صالح', 'Invalid relay URL': 'عنوان URL للمرحل غير صالح',
'Muted words': 'الكلمات المحظورة', 'Muted words': 'الكلمات المحظورة',
'Add muted word': 'إضافة كلمة محظورة' 'Add muted word': 'إضافة كلمة محظورة',
'Zap Details': 'تفاصيل Zap'
} }
} }

View file

@ -684,6 +684,7 @@ export default {
'Warnung: Ändern Sie diese Einstellungen nicht leichtfertig, da dies Ihre grundlegende Erfahrung beeinträchtigen kann.', 'Warnung: Ändern Sie diese Einstellungen nicht leichtfertig, da dies Ihre grundlegende Erfahrung beeinträchtigen kann.',
'Invalid relay URL': 'Ungültige Relay-URL', 'Invalid relay URL': 'Ungültige Relay-URL',
'Muted words': 'Stummgeschaltete Wörter', 'Muted words': 'Stummgeschaltete Wörter',
'Add muted word': 'Stummgeschaltetes Wort hinzufügen' 'Add muted word': 'Stummgeschaltetes Wort hinzufügen',
'Zap Details': 'Zap-Details'
} }
} }

View file

@ -667,6 +667,7 @@ export default {
'Warning: Please do not modify these settings casually, as it may affect your basic experience.', 'Warning: Please do not modify these settings casually, as it may affect your basic experience.',
'Invalid relay URL': 'Invalid relay URL', 'Invalid relay URL': 'Invalid relay URL',
'Muted words': 'Muted words', 'Muted words': 'Muted words',
'Add muted word': 'Add muted word' 'Add muted word': 'Add muted word',
'Zap Details': 'Zap Details'
} }
} }

View file

@ -678,6 +678,7 @@ export default {
'Advertencia: No modifiques estas configuraciones a la ligera, ya que puede afectar tu experiencia básica.', 'Advertencia: No modifiques estas configuraciones a la ligera, ya que puede afectar tu experiencia básica.',
'Invalid relay URL': 'URL de relé no válida', 'Invalid relay URL': 'URL de relé no válida',
'Muted words': 'Palabras silenciadas', 'Muted words': 'Palabras silenciadas',
'Add muted word': 'Agregar palabra silenciada' 'Add muted word': 'Agregar palabra silenciada',
'Zap Details': 'Detalles del Zap'
} }
} }

View file

@ -673,6 +673,7 @@ export default {
'هشدار: لطفاً این تنظیمات را به صورت تصادفی تغییر ندهید، ممکن است بر تجربه اولیه شما تأثیر بگذارد.', 'هشدار: لطفاً این تنظیمات را به صورت تصادفی تغییر ندهید، ممکن است بر تجربه اولیه شما تأثیر بگذارد.',
'Invalid relay URL': 'آدرس URL رله نامعتبر است', 'Invalid relay URL': 'آدرس URL رله نامعتبر است',
'Muted words': 'کلمات بی‌صدا شده', 'Muted words': 'کلمات بی‌صدا شده',
'Add muted word': 'افزودن کلمه بی‌صدا' 'Add muted word': 'افزودن کلمه بی‌صدا',
'Zap Details': 'جزئیات زپ'
} }
} }

View file

@ -682,6 +682,7 @@ export default {
'Attention : Ne modifiez pas ces paramètres à la légère, car cela pourrait affecter votre expérience de base.', 'Attention : Ne modifiez pas ces paramètres à la légère, car cela pourrait affecter votre expérience de base.',
'Invalid relay URL': 'URL de relais non valide', 'Invalid relay URL': 'URL de relais non valide',
'Muted words': 'Mots masqués', 'Muted words': 'Mots masqués',
'Add muted word': 'Ajouter un mot masqué' 'Add muted word': 'Ajouter un mot masqué',
'Zap Details': 'Détails du Zap'
} }
} }

View file

@ -674,6 +674,7 @@ export default {
'चेतावनी: कृपया इन सेटिंग्स को बेतरतीब ढंग से संशोधित न करें, क्योंकि यह आपके बुनियादी अनुभव को प्रभावित कर सकता है।', 'चेतावनी: कृपया इन सेटिंग्स को बेतरतीब ढंग से संशोधित न करें, क्योंकि यह आपके बुनियादी अनुभव को प्रभावित कर सकता है।',
'Invalid relay URL': 'अमान्य रिले URL', 'Invalid relay URL': 'अमान्य रिले URL',
'Muted words': 'म्यूट किए गए शब्द', 'Muted words': 'म्यूट किए गए शब्द',
'Add muted word': 'म्यूट शब्द जोड़ें' 'Add muted word': 'म्यूट शब्द जोड़ें',
'Zap Details': 'जैप विवरण'
} }
} }

View file

@ -667,6 +667,7 @@ export default {
'Figyelmeztetés: Ne módosítsa ezeket a beállításokat meggondolatlanul, mert ez befolyásolhatja az alapvető élményt.', 'Figyelmeztetés: Ne módosítsa ezeket a beállításokat meggondolatlanul, mert ez befolyásolhatja az alapvető élményt.',
'Invalid relay URL': 'Érvénytelen továbbító URL', 'Invalid relay URL': 'Érvénytelen továbbító URL',
'Muted words': 'Némított szavak', 'Muted words': 'Némított szavak',
'Add muted word': 'Némított szó hozzáadása' 'Add muted word': 'Némított szó hozzáadása',
'Zap Details': 'Zap Részletek'
} }
} }

View file

@ -678,6 +678,7 @@ export default {
"Attenzione: Non modificare queste impostazioni alla leggera, potrebbe influire sull'esperienza di base.", "Attenzione: Non modificare queste impostazioni alla leggera, potrebbe influire sull'esperienza di base.",
'Invalid relay URL': 'URL relay non valido', 'Invalid relay URL': 'URL relay non valido',
'Muted words': 'Parole silenziate', 'Muted words': 'Parole silenziate',
'Add muted word': 'Aggiungi parola silenziata' 'Add muted word': 'Aggiungi parola silenziata',
'Zap Details': 'Dettagli Zap'
} }
} }

View file

@ -672,6 +672,7 @@ export default {
'警告:これらの設定を無闇に変更しないでください。基本的な体験に影響を与える可能性があります。', '警告:これらの設定を無闇に変更しないでください。基本的な体験に影響を与える可能性があります。',
'Invalid relay URL': '無効なリレーURL', 'Invalid relay URL': '無効なリレーURL',
'Muted words': 'ミュートワード', 'Muted words': 'ミュートワード',
'Add muted word': 'ミュートワードを追加' 'Add muted word': 'ミュートワードを追加',
'Zap Details': 'Zapの詳細'
} }
} }

View file

@ -668,6 +668,7 @@ export default {
'경고: 이러한 설정을 임의로 수정하지 마십시오. 기본 경험에 영향을 줄 수 있습니다.', '경고: 이러한 설정을 임의로 수정하지 마십시오. 기본 경험에 영향을 줄 수 있습니다.',
'Invalid relay URL': '유효하지 않은 릴레이 URL', 'Invalid relay URL': '유효하지 않은 릴레이 URL',
'Muted words': '차단 단어', 'Muted words': '차단 단어',
'Add muted word': '차단 단어 추가' 'Add muted word': '차단 단어 추가',
'Zap Details': '잽 세부 정보'
} }
} }

View file

@ -679,6 +679,7 @@ export default {
'Ostrzeżenie: Nie modyfikuj tych ustawień pochopnie, może to wpłynąć na podstawowe doświadczenie.', 'Ostrzeżenie: Nie modyfikuj tych ustawień pochopnie, może to wpłynąć na podstawowe doświadczenie.',
'Invalid relay URL': 'Nieprawidłowy adres URL przekaźnika', 'Invalid relay URL': 'Nieprawidłowy adres URL przekaźnika',
'Muted words': 'Wyciszone słowa', 'Muted words': 'Wyciszone słowa',
'Add muted word': 'Dodaj wyciszone słowo' 'Add muted word': 'Dodaj wyciszone słowo',
'Zap Details': 'Szczegóły zapu'
} }
} }

View file

@ -675,6 +675,7 @@ export default {
'Aviso: Não modifique essas configurações casualmente, pois pode afetar sua experiência básica.', 'Aviso: Não modifique essas configurações casualmente, pois pode afetar sua experiência básica.',
'Invalid relay URL': 'URL de relé inválida', 'Invalid relay URL': 'URL de relé inválida',
'Muted words': 'Palavras silenciadas', 'Muted words': 'Palavras silenciadas',
'Add muted word': 'Adicionar palavra silenciada' 'Add muted word': 'Adicionar palavra silenciada',
'Zap Details': 'Detalhes do Zap'
} }
} }

View file

@ -678,6 +678,7 @@ export default {
'Aviso: Não modifique estas configurações casualmente, pois pode afetar a sua experiência básica.', 'Aviso: Não modifique estas configurações casualmente, pois pode afetar a sua experiência básica.',
'Invalid relay URL': 'URL de relay inválido', 'Invalid relay URL': 'URL de relay inválido',
'Muted words': 'Palavras silenciadas', 'Muted words': 'Palavras silenciadas',
'Add muted word': 'Adicionar palavra silenciada' 'Add muted word': 'Adicionar palavra silenciada',
'Zap Details': 'Detalhes do Zap'
} }
} }

View file

@ -678,6 +678,7 @@ export default {
'Предупреждение: Не изменяйте эти настройки без необходимости, это может повлиять на базовый опыт использования.', 'Предупреждение: Не изменяйте эти настройки без необходимости, это может повлиять на базовый опыт использования.',
'Invalid relay URL': 'Неверный URL реле', 'Invalid relay URL': 'Неверный URL реле',
'Muted words': 'Заблокированные слова', 'Muted words': 'Заблокированные слова',
'Add muted word': 'Добавить заблокированное слово' 'Add muted word': 'Добавить заблокированное слово',
'Zap Details': 'Детали запа'
} }
} }

View file

@ -663,6 +663,7 @@ export default {
'คำเตือน: กรุณาอย่าแก้ไขการตั้งค่าเหล่านี้โดยไม่ระมัดระวัง เพราะอาจส่งผลต่อประสบการณ์พื้นฐานของคุณ', 'คำเตือน: กรุณาอย่าแก้ไขการตั้งค่าเหล่านี้โดยไม่ระมัดระวัง เพราะอาจส่งผลต่อประสบการณ์พื้นฐานของคุณ',
'Invalid relay URL': 'URL รีเลย์ไม่ถูกต้อง', 'Invalid relay URL': 'URL รีเลย์ไม่ถูกต้อง',
'Muted words': 'คำที่ถูกปิดเสียง', 'Muted words': 'คำที่ถูกปิดเสียง',
'Add muted word': 'เพิ่มคำที่ถูกปิดเสียง' 'Add muted word': 'เพิ่มคำที่ถูกปิดเสียง',
'Zap Details': 'รายละเอียดซาตส์'
} }
} }

View file

@ -646,6 +646,7 @@ export default {
'Default relays warning': '警告:請不要隨意修改這些設定,可能會影響基礎體驗。', 'Default relays warning': '警告:請不要隨意修改這些設定,可能會影響基礎體驗。',
'Invalid relay URL': '無效的中繼地址', 'Invalid relay URL': '無效的中繼地址',
'Muted words': '屏蔽詞', 'Muted words': '屏蔽詞',
'Add muted word': '添加屏蔽詞' 'Add muted word': '添加屏蔽詞',
'Zap Details': '打閃詳情'
} }
} }

View file

@ -651,6 +651,7 @@ export default {
'Default relays warning': '警告:请不要随意修改这些设置,可能会影响您的基本体验。', 'Default relays warning': '警告:请不要随意修改这些设置,可能会影响您的基本体验。',
'Invalid relay URL': '无效的中继地址', 'Invalid relay URL': '无效的中继地址',
'Muted words': '屏蔽词', 'Muted words': '屏蔽词',
'Add muted word': '添加屏蔽词' 'Add muted word': '添加屏蔽词',
'Zap Details': '打闪详情'
} }
} }

View file

@ -329,7 +329,7 @@ class StuffStatsService {
const info = getZapInfoFromEvent(evt) const info = getZapInfoFromEvent(evt)
if (!info) return if (!info) return
const { originalEventId, senderPubkey, invoice, amount, comment } = info const { originalEventId, senderPubkey, invoice, amount, comment } = info
if (!originalEventId || !senderPubkey) return if (!originalEventId || !senderPubkey || amount <= 0) return
return this.addZap( return this.addZap(
senderPubkey, senderPubkey,