feat: emoji packs

This commit is contained in:
codytseng 2025-11-07 22:36:07 +08:00
parent 0e550d2511
commit 1e2385da3b
41 changed files with 646 additions and 59 deletions

View file

@ -0,0 +1,103 @@
import { Button } from '@/components/ui/button'
import { getReplaceableCoordinateFromEvent } from '@/lib/event'
import { getEmojiPackInfoFromEvent } from '@/lib/event-metadata'
import { useEmojiPack } from '@/providers/EmojiPackProvider'
import { useNostr } from '@/providers/NostrProvider'
import { CheckIcon, Loader, PlusIcon } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import Image from '../Image'
export default function EmojiPack({ event, className }: { event: Event; className?: string }) {
const { t } = useTranslation()
const { pubkey: accountPubkey, checkLogin } = useNostr()
const { emojiPackCoordinateSet, addEmojiPack, removeEmojiPack } = useEmojiPack()
const [updating, setUpdating] = useState(false)
const { title, emojis } = useMemo(() => getEmojiPackInfoFromEvent(event), [event])
const coordinate = useMemo(() => getReplaceableCoordinateFromEvent(event), [event])
const isCollected = useMemo(() => {
return emojiPackCoordinateSet.has(coordinate)
}, [emojiPackCoordinateSet, coordinate])
const handleCollect = async (e: React.MouseEvent) => {
e.stopPropagation()
checkLogin(async () => {
if (isCollected) return
setUpdating(true)
try {
await addEmojiPack(event)
toast.success(t('Emoji pack added'))
} catch (error) {
toast.error(t('Add emoji pack failed') + ': ' + (error as Error).message)
} finally {
setUpdating(false)
}
})
}
const handleRemoveCollect = async (e: React.MouseEvent) => {
e.stopPropagation()
checkLogin(async () => {
if (!isCollected) return
setUpdating(true)
try {
await removeEmojiPack(event)
toast.success(t('Emoji pack removed'))
} catch (error) {
toast.error(t('Remove emoji pack failed') + ': ' + (error as Error).message)
} finally {
setUpdating(false)
}
})
}
return (
<div className={className}>
<div className="flex items-center justify-between mb-2">
<h3 className="text-2xl font-semibold">{title}</h3>
{accountPubkey && (
<Button
variant={isCollected ? 'secondary' : 'outline'}
size="sm"
onClick={isCollected ? handleRemoveCollect : handleCollect}
disabled={updating}
className="shrink-0"
>
{updating ? (
<Loader className="animate-spin mr-1" />
) : isCollected ? (
<CheckIcon />
) : (
<PlusIcon />
)}
{updating
? isCollected
? t('Removing...')
: t('Adding...')
: isCollected
? t('Added')
: t('Add')}
</Button>
)}
</div>
<div className="flex flex-wrap gap-1">
{emojis.map((emoji, index) => (
<Image
key={`emoji-${index}`}
image={{ url: emoji.url, pubkey: event.pubkey }}
className="size-14 object-contain"
classNames={{
wrapper: 'size-14 flex items-center justify-center p-1',
errorPlaceholder: 'size-14'
}}
hideIfError
/>
))}
</div>
</div>
)
}

View file

@ -19,6 +19,7 @@ import TranslateButton from '../TranslateButton'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
import CommunityDefinition from './CommunityDefinition'
import EmojiPack from './EmojiPack'
import GroupMetadata from './GroupMetadata'
import Highlight from './Highlight'
import IValue from './IValue'
@ -102,6 +103,8 @@ export default function Note({
content = <VideoNote className="mt-2" event={event} />
} else if (event.kind === ExtendedKind.RELAY_REVIEW) {
content = <RelayReview className="mt-2" event={event} />
} else if (event.kind === kinds.Emojisets) {
content = <EmojiPack className="mt-2" event={event} />
} else {
content = <Content className="mt-2" event={event} />
}