feat: embedded emoji

This commit is contained in:
codytseng
2025-04-17 17:09:22 +08:00
parent c40609c8ac
commit 319ae5a0ba
10 changed files with 93 additions and 14 deletions

View File

@@ -1,4 +1,5 @@
import {
EmbeddedEmojiParser,
EmbeddedEventParser,
EmbeddedHashtagParser,
EmbeddedImageParser,
@@ -8,7 +9,7 @@ import {
EmbeddedWebsocketUrlParser,
parseContent
} from '@/lib/content-parser'
import { isNsfwEvent } from '@/lib/event'
import { extractEmojiInfosFromTags, isNsfwEvent } from '@/lib/event'
import { extractImageInfoFromTag } from '@/lib/tag'
import { cn } from '@/lib/utils'
import { TImageInfo } from '@/types'
@@ -21,6 +22,7 @@ import {
EmbeddedNote,
EmbeddedWebsocketUrl
} from '../Embedded'
import Emoji from '../Emoji'
import ImageGallery from '../ImageGallery'
import VideoPlayer from '../VideoPlayer'
import WebPreview from '../WebPreview'
@@ -42,13 +44,16 @@ const Content = memo(
EmbeddedWebsocketUrlParser,
EmbeddedEventParser,
EmbeddedMentionParser,
EmbeddedHashtagParser
EmbeddedHashtagParser,
EmbeddedEmojiParser
])
const imageInfos = event.tags
.map((tag) => extractImageInfoFromTag(tag))
.filter(Boolean) as TImageInfo[]
const emojiInfos = extractEmojiInfosFromTags(event.tags)
const lastNormalUrlNode = nodes.findLast((node) => node.type === 'url')
const lastNormalUrl =
typeof lastNormalUrlNode?.data === 'string' ? lastNormalUrlNode.data : undefined
@@ -107,6 +112,12 @@ const Content = memo(
if (node.type === 'hashtag') {
return <EmbeddedHashtag hashtag={node.data} key={index} />
}
if (node.type === 'emoji') {
const shortcode = node.data.split(':')[1]
const emoji = emojiInfos.find((e) => e.shortcode === shortcode)
if (!emoji) return node.data
return <Emoji emoji={emoji} key={index} className="size-4" />
}
return null
})}
{lastNormalUrl && (

View File

@@ -1,15 +1,18 @@
import {
EmbeddedEmojiParser,
EmbeddedEventParser,
EmbeddedImageParser,
EmbeddedMentionParser,
EmbeddedVideoParser,
parseContent
} from '@/lib/content-parser'
import { extractEmojiInfosFromTags } from '@/lib/event'
import { cn } from '@/lib/utils'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { EmbeddedMentionText } from '../Embedded'
import Emoji from '../Emoji'
export default function ContentPreview({
event,
@@ -26,10 +29,13 @@ export default function ContentPreview({
EmbeddedImageParser,
EmbeddedVideoParser,
EmbeddedEventParser,
EmbeddedMentionParser
EmbeddedMentionParser,
EmbeddedEmojiParser
])
}, [event])
const emojiInfos = extractEmojiInfosFromTags(event?.tags)
return (
<div className={cn('pointer-events-none', className)}>
{nodes.map((node, index) => {
@@ -48,6 +54,12 @@ export default function ContentPreview({
if (node.type === 'mention') {
return <EmbeddedMentionText key={index} userId={node.data.split(':')[1]} />
}
if (node.type === 'emoji') {
const shortcode = node.data.split(':')[1]
const emoji = emojiInfos.find((e) => e.shortcode === shortcode)
if (!emoji) return node.data
return <Emoji key={index} emoji={emoji} />
}
})}
</div>
)

View File

@@ -0,0 +1,29 @@
import { cn } from '@/lib/utils'
import { TEmoji } from '@/types'
import { HTMLAttributes, useState } from 'react'
export default function Emoji({
emoji,
className = ''
}: HTMLAttributes<HTMLDivElement> & {
className?: string
emoji: TEmoji
}) {
const [hasError, setHasError] = useState(false)
if (hasError) return `:${emoji.shortcode}:`
return (
<img
src={emoji.url}
alt={emoji.shortcode}
className={cn('inline-block size-4', className)}
onLoad={() => {
setHasError(false)
}}
onError={() => {
setHasError(true)
}}
/>
)
}

View File

@@ -2,7 +2,7 @@ import Image from '@/components/Image'
import { ExtendedKind } from '@/constants'
import { useFetchEvent } from '@/hooks'
import { toNote } from '@/lib/link'
import { extractEmojiFromEventTags, tagNameEquals } from '@/lib/tag'
import { tagNameEquals } from '@/lib/tag'
import { cn } from '@/lib/utils'
import { useSecondaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider'
@@ -37,7 +37,8 @@ export function ReactionNotification({
const emojiName = /^:([^:]+):$/.exec(notification.content)?.[1]
if (emojiName) {
const emojiUrl = extractEmojiFromEventTags(emojiName, notification.tags)
const emojiTag = notification.tags.find((tag) => tag[0] === 'emoji' && tag[1] === emojiName)
const emojiUrl = emojiTag?.[2]
if (emojiUrl) {
return (
<Image

View File

@@ -1,11 +1,12 @@
import {
EmbeddedEmojiParser,
EmbeddedHashtagParser,
EmbeddedMentionParser,
EmbeddedNormalUrlParser,
EmbeddedWebsocketUrlParser,
parseContent
} from '@/lib/content-parser'
import { extractImageInfosFromEventTags, isNsfwEvent } from '@/lib/event'
import { extractEmojiInfosFromTags, extractImageInfosFromEventTags, isNsfwEvent } from '@/lib/event'
import { cn } from '@/lib/utils'
import { Event } from 'nostr-tools'
import { memo, useMemo } from 'react'
@@ -16,6 +17,7 @@ import {
EmbeddedWebsocketUrl
} from '../Embedded'
import { ImageCarousel } from '../ImageCarousel'
import Emoji from '../Emoji'
const PictureContent = memo(({ event, className }: { event: Event; className?: string }) => {
const images = useMemo(() => extractImageInfosFromEventTags(event), [event])
@@ -25,9 +27,12 @@ const PictureContent = memo(({ event, className }: { event: Event; className?: s
EmbeddedNormalUrlParser,
EmbeddedWebsocketUrlParser,
EmbeddedHashtagParser,
EmbeddedMentionParser
EmbeddedMentionParser,
EmbeddedEmojiParser
])
const emojiInfos = extractEmojiInfosFromTags(event.tags)
return (
<div className={cn('text-wrap break-words whitespace-pre-wrap space-y-2', className)}>
<ImageCarousel images={images} isNsfw={isNsfw} />
@@ -48,6 +53,12 @@ const PictureContent = memo(({ event, className }: { event: Event; className?: s
if (node.type === 'mention') {
return <EmbeddedMention key={index} userId={node.data.split(':')[1]} />
}
if (node.type === 'emoji') {
const shortcode = node.data.split(':')[1]
const emoji = emojiInfos.find((e) => e.shortcode === shortcode)
if (!emoji) return node.data
return <Emoji key={index} emoji={emoji} />
}
})}
</div>
</div>

View File

@@ -56,7 +56,7 @@ export default function WebPreview({
{image && (
<Image
image={{ url: image }}
className={`rounded-lg aspect-[4/3] object-cover bg-foreground ${size === 'normal' ? 'h-44' : 'h-24'}`}
className={`rounded-lg aspect-[4/3] xl:aspect-video object-cover bg-foreground ${size === 'normal' ? 'h-44' : 'h-24'}`}
hideIfError
/>
)}