Bpistle/src/components/Content/index.tsx
2025-04-17 17:09:22 +08:00

135 lines
4 KiB
TypeScript

import {
EmbeddedEmojiParser,
EmbeddedEventParser,
EmbeddedHashtagParser,
EmbeddedImageParser,
EmbeddedMentionParser,
EmbeddedNormalUrlParser,
EmbeddedVideoParser,
EmbeddedWebsocketUrlParser,
parseContent
} from '@/lib/content-parser'
import { extractEmojiInfosFromTags, isNsfwEvent } from '@/lib/event'
import { extractImageInfoFromTag } from '@/lib/tag'
import { cn } from '@/lib/utils'
import { TImageInfo } from '@/types'
import { Event } from 'nostr-tools'
import { memo } from 'react'
import {
EmbeddedHashtag,
EmbeddedMention,
EmbeddedNormalUrl,
EmbeddedNote,
EmbeddedWebsocketUrl
} from '../Embedded'
import Emoji from '../Emoji'
import ImageGallery from '../ImageGallery'
import VideoPlayer from '../VideoPlayer'
import WebPreview from '../WebPreview'
const Content = memo(
({
event,
className,
size = 'normal'
}: {
event: Event
className?: string
size?: 'normal' | 'small'
}) => {
const nodes = parseContent(event.content, [
EmbeddedImageParser,
EmbeddedVideoParser,
EmbeddedNormalUrlParser,
EmbeddedWebsocketUrlParser,
EmbeddedEventParser,
EmbeddedMentionParser,
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
return (
<div className={cn('text-wrap break-words whitespace-pre-wrap', className)}>
{nodes.map((node, index) => {
if (node.type === 'text') {
return node.data
}
if (node.type === 'image' || node.type === 'images') {
const imageUrls = Array.isArray(node.data) ? node.data : [node.data]
const images = imageUrls.map(
(url) => imageInfos.find((image) => image.url === url) ?? { url }
)
return (
<ImageGallery
className={`${size === 'small' ? 'mt-1' : 'mt-2'}`}
key={index}
images={images}
isNsfw={isNsfwEvent(event)}
size={size}
/>
)
}
if (node.type === 'video') {
return (
<VideoPlayer
className={size === 'small' ? 'mt-1' : 'mt-2'}
key={index}
src={node.data}
isNsfw={isNsfwEvent(event)}
size={size}
/>
)
}
if (node.type === 'url') {
return <EmbeddedNormalUrl url={node.data} key={index} />
}
if (node.type === 'websocket-url') {
return <EmbeddedWebsocketUrl url={node.data} key={index} />
}
if (node.type === 'event') {
const id = node.data.split(':')[1]
return (
<EmbeddedNote
key={index}
noteId={id}
className={size === 'small' ? 'mt-1' : 'mt-2'}
/>
)
}
if (node.type === 'mention') {
return <EmbeddedMention key={index} userId={node.data.split(':')[1]} />
}
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 && (
<WebPreview
className={size === 'small' ? 'mt-1' : 'mt-2'}
url={lastNormalUrl}
size={size}
/>
)}
</div>
)
}
)
Content.displayName = 'Content'
export default Content