EmojiPack.tsx raw
1 import { Button } from '@/components/ui/button'
2 import { getReplaceableCoordinateFromEvent } from '@/lib/event'
3 import { getEmojiPackInfoFromEvent } from '@/lib/event-metadata'
4 import { useEmojiPack } from '@/providers/EmojiPackProvider'
5 import { useNostr } from '@/providers/NostrProvider'
6 import { CheckIcon, Loader, PlusIcon } from 'lucide-react'
7 import { Event } from 'nostr-tools'
8 import { useMemo, useState } from 'react'
9 import { useTranslation } from 'react-i18next'
10 import { toast } from 'sonner'
11 import Image from '../Image'
12
13 export default function EmojiPack({ event, className }: { event: Event; className?: string }) {
14 const { t } = useTranslation()
15 const { pubkey: accountPubkey, checkLogin } = useNostr()
16 const { emojiPackCoordinateSet, addEmojiPack, removeEmojiPack } = useEmojiPack()
17 const [updating, setUpdating] = useState(false)
18 const { title, emojis } = useMemo(() => getEmojiPackInfoFromEvent(event), [event])
19 const coordinate = useMemo(() => getReplaceableCoordinateFromEvent(event), [event])
20 const isCollected = useMemo(() => {
21 return emojiPackCoordinateSet.has(coordinate)
22 }, [emojiPackCoordinateSet, coordinate])
23
24 const handleCollect = async (e: React.MouseEvent) => {
25 e.stopPropagation()
26 checkLogin(async () => {
27 if (isCollected) return
28
29 setUpdating(true)
30 try {
31 await addEmojiPack(event)
32 toast.success(t('Emoji pack added'))
33 } catch (error) {
34 toast.error(t('Add emoji pack failed') + ': ' + (error as Error).message)
35 } finally {
36 setUpdating(false)
37 }
38 })
39 }
40
41 const handleRemoveCollect = async (e: React.MouseEvent) => {
42 e.stopPropagation()
43 checkLogin(async () => {
44 if (!isCollected) return
45
46 setUpdating(true)
47 try {
48 await removeEmojiPack(event)
49 toast.success(t('Emoji pack removed'))
50 } catch (error) {
51 toast.error(t('Remove emoji pack failed') + ': ' + (error as Error).message)
52 } finally {
53 setUpdating(false)
54 }
55 })
56 }
57
58 return (
59 <div className={className}>
60 <div className="flex items-center justify-between mb-2">
61 <h3 className="text-2xl font-semibold">{title}</h3>
62 {accountPubkey && (
63 <Button
64 variant={isCollected ? 'secondary' : 'outline'}
65 size="sm"
66 onClick={isCollected ? handleRemoveCollect : handleCollect}
67 disabled={updating}
68 className="shrink-0"
69 >
70 {updating ? (
71 <Loader className="animate-spin mr-1" />
72 ) : isCollected ? (
73 <CheckIcon />
74 ) : (
75 <PlusIcon />
76 )}
77 {updating
78 ? isCollected
79 ? t('Removing...')
80 : t('Adding...')
81 : isCollected
82 ? t('Added')
83 : t('Add')}
84 </Button>
85 )}
86 </div>
87 <div className="flex flex-wrap gap-1">
88 {emojis.map((emoji, index) => (
89 <Image
90 key={`emoji-${index}`}
91 image={{ url: emoji.url, pubkey: event.pubkey }}
92 className="size-14 object-contain"
93 classNames={{
94 wrapper: 'size-14 flex items-center justify-center p-1',
95 errorPlaceholder: 'size-14'
96 }}
97 hideIfError
98 />
99 ))}
100 </div>
101 </div>
102 )
103 }
104