feat: embedded emoji
This commit is contained in:
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
29
src/components/Emoji/index.tsx
Normal file
29
src/components/Emoji/index.tsx
Normal 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)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user