feat: enhance post content parsing and rendering (#263)
This commit is contained in:
@@ -1,19 +1,25 @@
|
||||
import { URL_REGEX } from '@/constants'
|
||||
import { isNsfwEvent, isPictureEvent } from '@/lib/event'
|
||||
import {
|
||||
EmbeddedEventParser,
|
||||
EmbeddedHashtagParser,
|
||||
EmbeddedImageParser,
|
||||
EmbeddedMentionParser,
|
||||
EmbeddedNormalUrlParser,
|
||||
EmbeddedVideoParser,
|
||||
EmbeddedWebsocketUrlParser,
|
||||
parseContent
|
||||
} from '@/lib/content-parser'
|
||||
import { isNsfwEvent } from '@/lib/event'
|
||||
import { extractImageInfoFromTag } from '@/lib/tag'
|
||||
import { isImage, isVideo } from '@/lib/url'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { TImageInfo } from '@/types'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { memo } from 'react'
|
||||
import {
|
||||
embedded,
|
||||
embeddedHashtagRenderer,
|
||||
embeddedNormalUrlRenderer,
|
||||
embeddedNostrNpubRenderer,
|
||||
embeddedNostrProfileRenderer,
|
||||
EmbeddedHashtag,
|
||||
EmbeddedMention,
|
||||
EmbeddedNormalUrl,
|
||||
EmbeddedNote,
|
||||
embeddedWebsocketUrlRenderer
|
||||
EmbeddedWebsocketUrl
|
||||
} from '../Embedded'
|
||||
import ImageGallery from '../ImageGallery'
|
||||
import VideoPlayer from '../VideoPlayer'
|
||||
@@ -29,115 +35,90 @@ const Content = memo(
|
||||
className?: string
|
||||
size?: 'normal' | 'small'
|
||||
}) => {
|
||||
const { content, images, videos, embeddedNotes, lastNonMediaUrl } = preprocess(event)
|
||||
const isNsfw = isNsfwEvent(event)
|
||||
const nodes = embedded(content, [
|
||||
embeddedNormalUrlRenderer,
|
||||
embeddedWebsocketUrlRenderer,
|
||||
embeddedHashtagRenderer,
|
||||
embeddedNostrNpubRenderer,
|
||||
embeddedNostrProfileRenderer
|
||||
const nodes = parseContent(event.content, [
|
||||
EmbeddedImageParser,
|
||||
EmbeddedVideoParser,
|
||||
EmbeddedNormalUrlParser,
|
||||
EmbeddedWebsocketUrlParser,
|
||||
EmbeddedEventParser,
|
||||
EmbeddedMentionParser,
|
||||
EmbeddedHashtagParser
|
||||
])
|
||||
|
||||
// Add images
|
||||
if (images.length) {
|
||||
nodes.push(
|
||||
<ImageGallery
|
||||
className={`${size === 'small' ? 'mt-1' : 'mt-2'}`}
|
||||
key={`image-gallery-${event.id}`}
|
||||
images={images}
|
||||
isNsfw={isNsfw}
|
||||
size={size}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const imageInfos = event.tags
|
||||
.map((tag) => extractImageInfoFromTag(tag))
|
||||
.filter(Boolean) as TImageInfo[]
|
||||
|
||||
// Add videos
|
||||
if (videos.length) {
|
||||
videos.forEach((src, index) => {
|
||||
nodes.push(
|
||||
<VideoPlayer
|
||||
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} />
|
||||
}
|
||||
return null
|
||||
})}
|
||||
{lastNormalUrl && (
|
||||
<WebPreview
|
||||
className={size === 'small' ? 'mt-1' : 'mt-2'}
|
||||
key={`video-${index}-${src}`}
|
||||
src={src}
|
||||
isNsfw={isNsfw}
|
||||
url={lastNormalUrl}
|
||||
size={size}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Add website preview
|
||||
if (lastNonMediaUrl) {
|
||||
nodes.push(
|
||||
<WebPreview
|
||||
className={size === 'small' ? 'mt-1' : 'mt-2'}
|
||||
key={`web-preview-${event.id}`}
|
||||
url={lastNonMediaUrl}
|
||||
size={size}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Add embedded notes
|
||||
if (embeddedNotes.length) {
|
||||
embeddedNotes.forEach((note, index) => {
|
||||
const id = note.split(':')[1]
|
||||
nodes.push(
|
||||
<EmbeddedNote
|
||||
key={`embedded-event-${index}`}
|
||||
noteId={id}
|
||||
className={size === 'small' ? 'mt-1' : 'mt-2'}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return <div className={cn('text-wrap break-words whitespace-pre-wrap', className)}>{nodes}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
Content.displayName = 'Content'
|
||||
export default Content
|
||||
|
||||
function preprocess(event: Event) {
|
||||
const content = event.content
|
||||
const urls = content.match(URL_REGEX) || []
|
||||
let lastNonMediaUrl: string | undefined
|
||||
|
||||
let c = content
|
||||
const imageUrls: string[] = []
|
||||
const videos: string[] = []
|
||||
|
||||
urls.forEach((url) => {
|
||||
if (isImage(url)) {
|
||||
c = c.replace(url, '').trim()
|
||||
imageUrls.push(url)
|
||||
} else if (isVideo(url)) {
|
||||
c = c.replace(url, '').trim()
|
||||
videos.push(url)
|
||||
} else {
|
||||
lastNonMediaUrl = url
|
||||
}
|
||||
})
|
||||
|
||||
const imageInfos = event.tags
|
||||
.map((tag) => extractImageInfoFromTag(tag))
|
||||
.filter(Boolean) as TImageInfo[]
|
||||
const images = isPictureEvent(event)
|
||||
? imageInfos
|
||||
: imageUrls.map((url) => {
|
||||
const imageInfo = imageInfos.find((info) => info.url === url)
|
||||
return imageInfo ?? { url }
|
||||
})
|
||||
|
||||
const embeddedNotes: string[] = []
|
||||
const embeddedNoteRegex = /nostr:(note1[a-z0-9]{58}|nevent1[a-z0-9]+|naddr1[a-z0-9]+)/g
|
||||
;(c.match(embeddedNoteRegex) || []).forEach((note) => {
|
||||
c = c.replace(note, '').trim()
|
||||
embeddedNotes.push(note)
|
||||
})
|
||||
|
||||
c = c.replace(/\n{3,}/g, '\n\n').trim()
|
||||
|
||||
return { content: c, images, videos, embeddedNotes, lastNonMediaUrl }
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { extractEmbeddedNotesFromContent, extractImagesFromContent } from '@/lib/event'
|
||||
import {
|
||||
EmbeddedEventParser,
|
||||
EmbeddedImageParser,
|
||||
EmbeddedMentionParser,
|
||||
EmbeddedVideoParser,
|
||||
parseContent
|
||||
} from '@/lib/content-parser'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
embedded,
|
||||
embeddedNostrNpubTextRenderer,
|
||||
embeddedNostrProfileTextRenderer
|
||||
} from '../Embedded'
|
||||
import { EmbeddedMentionText } from '../Embedded'
|
||||
|
||||
export default function ContentPreview({
|
||||
event,
|
||||
@@ -17,24 +19,36 @@ export default function ContentPreview({
|
||||
className?: string
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const content = useMemo(() => {
|
||||
if (!event) return `[${t('Not found the note')}]`
|
||||
const { contentWithoutEmbeddedNotes, embeddedNotes } = extractEmbeddedNotesFromContent(
|
||||
event.content
|
||||
)
|
||||
const { contentWithoutImages, images } = extractImagesFromContent(contentWithoutEmbeddedNotes)
|
||||
const contents = [contentWithoutImages]
|
||||
if (images?.length) {
|
||||
contents.push(`[${t('image')}]`)
|
||||
}
|
||||
if (embeddedNotes.length) {
|
||||
contents.push(`[${t('note')}]`)
|
||||
}
|
||||
return embedded(contents.join(' '), [
|
||||
embeddedNostrProfileTextRenderer,
|
||||
embeddedNostrNpubTextRenderer
|
||||
const nodes = useMemo(() => {
|
||||
if (!event) return [{ type: 'text', data: `[${t('Not found the note')}]` }]
|
||||
|
||||
return parseContent(event.content, [
|
||||
EmbeddedImageParser,
|
||||
EmbeddedVideoParser,
|
||||
EmbeddedEventParser,
|
||||
EmbeddedMentionParser
|
||||
])
|
||||
}, [event])
|
||||
|
||||
return <div className={cn('pointer-events-none', className)}>{content}</div>
|
||||
return (
|
||||
<div className={cn('pointer-events-none', className)}>
|
||||
{nodes.map((node, index) => {
|
||||
if (node.type === 'text') {
|
||||
return node.data
|
||||
}
|
||||
if (node.type === 'image' || node.type === 'images') {
|
||||
return index > 0 ? ` [${t('image')}]` : `[${t('image')}]`
|
||||
}
|
||||
if (node.type === 'video') {
|
||||
return index > 0 ? ` [${t('video')}]` : `[${t('video')}]`
|
||||
}
|
||||
if (node.type === 'event') {
|
||||
return index > 0 ? ` [${t('note')}]` : `[${t('note')}]`
|
||||
}
|
||||
if (node.type === 'mention') {
|
||||
return <EmbeddedMentionText key={index} userId={node.data.split(':')[1]} />
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { toNoteList } from '@/lib/link'
|
||||
import { SecondaryPageLink } from '@/PageManager'
|
||||
import { TEmbeddedRenderer } from './types'
|
||||
|
||||
export function EmbeddedHashtag({ hashtag }: { hashtag: string }) {
|
||||
return (
|
||||
@@ -9,14 +8,7 @@ export function EmbeddedHashtag({ hashtag }: { hashtag: string }) {
|
||||
to={toNoteList({ hashtag })}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
#{hashtag}
|
||||
{hashtag}
|
||||
</SecondaryPageLink>
|
||||
)
|
||||
}
|
||||
|
||||
export const embeddedHashtagRenderer: TEmbeddedRenderer = {
|
||||
regex: /#([\p{L}\p{N}\p{M}_]+)/gu,
|
||||
render: (hashtag: string, index: number) => {
|
||||
return <EmbeddedHashtag key={`hashtag-${index}-${hashtag}`} hashtag={hashtag} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Username, { SimpleUsername } from '../Username'
|
||||
import { TEmbeddedRenderer } from './types'
|
||||
|
||||
export function EmbeddedMention({ userId }: { userId: string }) {
|
||||
return (
|
||||
@@ -10,47 +9,3 @@ export function EmbeddedMention({ userId }: { userId: string }) {
|
||||
export function EmbeddedMentionText({ userId }: { userId: string }) {
|
||||
return <SimpleUsername userId={userId} showAt className="inline truncate" withoutSkeleton />
|
||||
}
|
||||
|
||||
export const embeddedNostrNpubRenderer: TEmbeddedRenderer = {
|
||||
regex: /(nostr:npub1[a-z0-9]{58})/g,
|
||||
render: (id: string, index: number) => {
|
||||
const npub1 = id.split(':')[1]
|
||||
return <EmbeddedMention key={`embedded-nostr-npub-${index}-${npub1}`} userId={npub1} />
|
||||
}
|
||||
}
|
||||
|
||||
export const embeddedNostrProfileRenderer: TEmbeddedRenderer = {
|
||||
regex: /(nostr:nprofile1[a-z0-9]+)/g,
|
||||
render: (id: string, index: number) => {
|
||||
const nprofile = id.split(':')[1]
|
||||
return <EmbeddedMention key={`embedded-nostr-profile-${index}-${nprofile}`} userId={nprofile} />
|
||||
}
|
||||
}
|
||||
|
||||
export const embeddedNpubRenderer: TEmbeddedRenderer = {
|
||||
regex: /(npub1[a-z0-9]{58})/g,
|
||||
render: (npub1: string, index: number) => {
|
||||
return <EmbeddedMention key={`embedded-npub-${index}-${npub1}`} userId={npub1} />
|
||||
}
|
||||
}
|
||||
|
||||
export const embeddedNostrNpubTextRenderer: TEmbeddedRenderer = {
|
||||
regex: /(nostr:npub1[a-z0-9]{58})/g,
|
||||
render: (id: string, index: number) => {
|
||||
const npub1 = id.split(':')[1]
|
||||
return <EmbeddedMentionText key={`embedded-nostr-npub-text-${index}-${npub1}`} userId={npub1} />
|
||||
}
|
||||
}
|
||||
|
||||
export const embeddedNostrProfileTextRenderer: TEmbeddedRenderer = {
|
||||
regex: /(nostr:nprofile1[a-z0-9]+)/g,
|
||||
render: (id: string, index: number) => {
|
||||
const nprofile = id.split(':')[1]
|
||||
return (
|
||||
<EmbeddedMentionText
|
||||
key={`embedded-nostr-profile-text-${index}-${nprofile}`}
|
||||
userId={nprofile}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { TEmbeddedRenderer } from './types'
|
||||
|
||||
export function EmbeddedNormalUrl({ url }: { url: string }) {
|
||||
return (
|
||||
<a
|
||||
@@ -13,10 +11,3 @@ export function EmbeddedNormalUrl({ url }: { url: string }) {
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export const embeddedNormalUrlRenderer: TEmbeddedRenderer = {
|
||||
regex: /(https?:\/\/[\w\p{L}\p{N}\p{M}&.-/?=#\-@%+_:!~*]+)/gu,
|
||||
render: (url: string, index: number) => {
|
||||
return <EmbeddedNormalUrl key={`normal-url-${index}-${url}`} url={url} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { toRelay } from '@/lib/link'
|
||||
import { TEmbeddedRenderer } from './types'
|
||||
|
||||
export function EmbeddedWebsocketUrl({ url }: { url: string }) {
|
||||
const { push } = useSecondaryPage()
|
||||
@@ -17,10 +16,3 @@ export function EmbeddedWebsocketUrl({ url }: { url: string }) {
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export const embeddedWebsocketUrlRenderer: TEmbeddedRenderer = {
|
||||
regex: /(wss?:\/\/[\w\p{L}\p{N}\p{M}&.-/?=#\-@%+_:!~*]+)/gu,
|
||||
render: (url: string, index: number) => {
|
||||
return <EmbeddedWebsocketUrl key={`websocket-url-${index}-${url}`} url={url} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,3 @@ export * from './EmbeddedMention'
|
||||
export * from './EmbeddedNormalUrl'
|
||||
export * from './EmbeddedNote'
|
||||
export * from './EmbeddedWebsocketUrl'
|
||||
|
||||
import reactStringReplace from 'react-string-replace'
|
||||
import { TEmbeddedRenderer } from './types'
|
||||
|
||||
export function embedded(content: string, renderers: TEmbeddedRenderer[]) {
|
||||
let nodes: React.ReactNode[] = [content]
|
||||
|
||||
renderers.forEach((renderer) => {
|
||||
nodes = reactStringReplace(nodes, renderer.regex, renderer.render)
|
||||
})
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export type TEmbeddedRenderer = {
|
||||
regex: RegExp
|
||||
render: (match: string, index: number) => JSX.Element
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
import {
|
||||
EmbeddedHashtagParser,
|
||||
EmbeddedMentionParser,
|
||||
EmbeddedNormalUrlParser,
|
||||
EmbeddedWebsocketUrlParser,
|
||||
parseContent
|
||||
} from '@/lib/content-parser'
|
||||
import { extractImageInfosFromEventTags, isNsfwEvent } from '@/lib/event'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { memo, ReactNode, useMemo } from 'react'
|
||||
import { memo, useMemo } from 'react'
|
||||
import {
|
||||
embedded,
|
||||
embeddedHashtagRenderer,
|
||||
embeddedNormalUrlRenderer,
|
||||
embeddedNostrNpubRenderer,
|
||||
embeddedNostrProfileRenderer,
|
||||
embeddedWebsocketUrlRenderer
|
||||
EmbeddedHashtag,
|
||||
EmbeddedMention,
|
||||
EmbeddedNormalUrl,
|
||||
EmbeddedWebsocketUrl
|
||||
} from '../Embedded'
|
||||
import { ImageCarousel } from '../ImageCarousel'
|
||||
|
||||
@@ -16,24 +21,35 @@ const PictureContent = memo(({ event, className }: { event: Event; className?: s
|
||||
const images = useMemo(() => extractImageInfosFromEventTags(event), [event])
|
||||
const isNsfw = isNsfwEvent(event)
|
||||
|
||||
const nodes: ReactNode[] = [
|
||||
<ImageCarousel key={`${event.id}-image-gallery`} images={images} isNsfw={isNsfw} />
|
||||
]
|
||||
nodes.push(
|
||||
<div key={`${event.id}-content`} className="px-4">
|
||||
{embedded(event.content, [
|
||||
embeddedNormalUrlRenderer,
|
||||
embeddedWebsocketUrlRenderer,
|
||||
embeddedHashtagRenderer,
|
||||
embeddedNostrNpubRenderer,
|
||||
embeddedNostrProfileRenderer
|
||||
])}
|
||||
</div>
|
||||
)
|
||||
const nodes = parseContent(event.content, [
|
||||
EmbeddedNormalUrlParser,
|
||||
EmbeddedWebsocketUrlParser,
|
||||
EmbeddedHashtagParser,
|
||||
EmbeddedMentionParser
|
||||
])
|
||||
|
||||
return (
|
||||
<div className={cn('text-wrap break-words whitespace-pre-wrap space-y-2', className)}>
|
||||
{nodes}
|
||||
<ImageCarousel images={images} isNsfw={isNsfw} />
|
||||
<div className="px-4">
|
||||
{nodes.map((node, index) => {
|
||||
if (node.type === 'text') {
|
||||
return node.data
|
||||
}
|
||||
if (node.type === 'url') {
|
||||
return <EmbeddedNormalUrl key={index} url={node.data} />
|
||||
}
|
||||
if (node.type === 'websocket-url') {
|
||||
return <EmbeddedWebsocketUrl key={index} url={node.data} />
|
||||
}
|
||||
if (node.type === 'hashtag') {
|
||||
return <EmbeddedHashtag key={index} hashtag={node.data} />
|
||||
}
|
||||
if (node.type === 'mention') {
|
||||
return <EmbeddedMention key={index} userId={node.data.split(':')[1]} />
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EmbeddedHashtagParser, EmbeddedMentionParser, parseContent } from '@/lib/content-parser'
|
||||
import { extractImageInfosFromEventTags } from '@/lib/event'
|
||||
import { toNote } from '@/lib/link'
|
||||
import { tagNameEquals } from '@/lib/tag'
|
||||
@@ -6,16 +7,11 @@ import { useSecondaryPage } from '@/PageManager'
|
||||
import { Images } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
import {
|
||||
embedded,
|
||||
embeddedHashtagRenderer,
|
||||
embeddedNostrNpubRenderer,
|
||||
embeddedNostrProfileRenderer
|
||||
} from '../Embedded'
|
||||
import { EmbeddedHashtag, EmbeddedMention } from '../Embedded'
|
||||
import Image from '../Image'
|
||||
import LikeButton from '../NoteStats/LikeButton'
|
||||
import UserAvatar from '../UserAvatar'
|
||||
import Username from '../Username'
|
||||
import LikeButton from '../NoteStats/LikeButton'
|
||||
|
||||
export default function PictureNoteCard({
|
||||
event,
|
||||
@@ -27,12 +23,21 @@ export default function PictureNoteCard({
|
||||
const { push } = useSecondaryPage()
|
||||
const images = useMemo(() => extractImageInfosFromEventTags(event), [event])
|
||||
const title = useMemo(() => {
|
||||
const title = event.tags.find(tagNameEquals('title'))?.[1] ?? event.content
|
||||
return embedded(title, [
|
||||
embeddedNostrNpubRenderer,
|
||||
embeddedNostrProfileRenderer,
|
||||
embeddedHashtagRenderer
|
||||
const nodes = parseContent(event.tags.find(tagNameEquals('title'))?.[1] ?? event.content, [
|
||||
EmbeddedMentionParser,
|
||||
EmbeddedHashtagParser
|
||||
])
|
||||
return nodes.map((node, index) => {
|
||||
if (node.type === 'text') {
|
||||
return node.data
|
||||
}
|
||||
if (node.type === 'mention') {
|
||||
return <EmbeddedMention key={index} userId={node.data.split(':')[1]} />
|
||||
}
|
||||
if (node.type === 'hashtag') {
|
||||
return <EmbeddedHashtag key={index} hashtag={node.data} />
|
||||
}
|
||||
})
|
||||
}, [event])
|
||||
if (!images.length) return null
|
||||
|
||||
|
||||
@@ -1,25 +1,46 @@
|
||||
import {
|
||||
EmbeddedHashtagParser,
|
||||
EmbeddedMentionParser,
|
||||
EmbeddedNormalUrlParser,
|
||||
EmbeddedWebsocketUrlParser,
|
||||
parseContent
|
||||
} from '@/lib/content-parser'
|
||||
import { useMemo } from 'react'
|
||||
import {
|
||||
embedded,
|
||||
embeddedHashtagRenderer,
|
||||
embeddedNormalUrlRenderer,
|
||||
embeddedNostrNpubRenderer,
|
||||
embeddedNpubRenderer,
|
||||
embeddedWebsocketUrlRenderer
|
||||
EmbeddedHashtag,
|
||||
EmbeddedMention,
|
||||
EmbeddedNormalUrl,
|
||||
EmbeddedWebsocketUrl
|
||||
} from '../Embedded'
|
||||
|
||||
export default function ProfileAbout({ about, className }: { about?: string; className?: string }) {
|
||||
const nodes = useMemo(() => {
|
||||
return about
|
||||
? embedded(about, [
|
||||
embeddedWebsocketUrlRenderer,
|
||||
embeddedNormalUrlRenderer,
|
||||
embeddedHashtagRenderer,
|
||||
embeddedNostrNpubRenderer,
|
||||
embeddedNpubRenderer
|
||||
])
|
||||
: null
|
||||
const aboutNodes = useMemo(() => {
|
||||
if (!about) return null
|
||||
|
||||
const nodes = parseContent(about, [
|
||||
EmbeddedWebsocketUrlParser,
|
||||
EmbeddedNormalUrlParser,
|
||||
EmbeddedHashtagParser,
|
||||
EmbeddedMentionParser
|
||||
])
|
||||
return nodes.map((node, index) => {
|
||||
if (node.type === 'text') {
|
||||
return node.data
|
||||
}
|
||||
if (node.type === 'url') {
|
||||
return <EmbeddedNormalUrl key={index} url={node.data} />
|
||||
}
|
||||
if (node.type === 'websocket-url') {
|
||||
return <EmbeddedWebsocketUrl key={index} url={node.data} />
|
||||
}
|
||||
if (node.type === 'hashtag') {
|
||||
return <EmbeddedHashtag key={index} hashtag={node.data} />
|
||||
}
|
||||
if (node.type === 'mention') {
|
||||
return <EmbeddedMention key={index} userId={node.data.split(':')[1]} />
|
||||
}
|
||||
})
|
||||
}, [about])
|
||||
|
||||
return <div className={className}>{nodes}</div>
|
||||
return <div className={className}>{aboutNodes}</div>
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function WebPreview({
|
||||
{image && (
|
||||
<Image
|
||||
image={{ url: image }}
|
||||
className={`rounded-lg ${size === 'normal' ? 'h-44' : 'h-24'}`}
|
||||
className={`rounded-lg aspect-[4/3] object-cover bg-foreground ${size === 'normal' ? 'h-44' : 'h-24'}`}
|
||||
hideIfError
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user