diff --git a/src/components/PostEditor/PostTextarea/Mention/MentionNode.tsx b/src/components/PostEditor/PostTextarea/Mention/MentionNode.tsx
index bfcf619..d5b3e60 100644
--- a/src/components/PostEditor/PostTextarea/Mention/MentionNode.tsx
+++ b/src/components/PostEditor/PostTextarea/Mention/MentionNode.tsx
@@ -1,3 +1,4 @@
+import TextWithEmojis from '@/components/TextWithEmojis'
import { useFetchProfile } from '@/hooks'
import { formatUserId } from '@/lib/pubkey'
import { cn } from '@/lib/utils'
@@ -11,7 +12,11 @@ export default function MentionNode(props: NodeViewRendererProps & { selected: b
className={cn('inline text-primary', props.selected ? 'bg-primary/20 rounded-sm' : '')}
>
{'@'}
- {profile ? profile.username : formatUserId(props.node.attrs.id)}
+ {profile ? (
+
+ ) : (
+ formatUserId(props.node.attrs.id)
+ )}
)
}
diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx
index 27516b5..4cc499d 100644
--- a/src/components/Profile/index.tsx
+++ b/src/components/Profile/index.tsx
@@ -109,7 +109,7 @@ export default function Profile({ id }: { id?: string }) {
}
if (!profile) return
- const { banner, username, about, pubkey, website, lightningAddress } = profile
+ const { banner, username, about, pubkey, website, lightningAddress, emojis } = profile
return (
<>
@@ -161,6 +161,7 @@ export default function Profile({ id }: { id?: string }) {
diff --git a/src/components/ProfileAbout/index.tsx b/src/components/ProfileAbout/index.tsx
index 2f733c6..f0637a0 100644
--- a/src/components/ProfileAbout/index.tsx
+++ b/src/components/ProfileAbout/index.tsx
@@ -1,4 +1,5 @@
import {
+ EmbeddedEmojiParser,
EmbeddedHashtagParser,
EmbeddedMentionParser,
EmbeddedUrlParser,
@@ -7,13 +8,23 @@ import {
} from '@/lib/content-parser'
import { detectLanguage } from '@/lib/utils'
import { useTranslationService } from '@/providers/TranslationServiceProvider'
+import { TEmoji } from '@/types'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { EmbeddedHashtag, EmbeddedMention, EmbeddedWebsocketUrl } from '../Embedded'
+import Emoji from '../Emoji'
import ExternalLink from '../ExternalLink'
-export default function ProfileAbout({ about, className }: { about?: string; className?: string }) {
+export default function ProfileAbout({
+ about,
+ emojis,
+ className
+}: {
+ about?: string
+ emojis?: TEmoji[]
+ className?: string
+}) {
const { t, i18n } = useTranslation()
const { translateText } = useTranslationService()
const needTranslation = useMemo(() => {
@@ -31,8 +42,16 @@ export default function ProfileAbout({ about, className }: { about?: string; cla
EmbeddedMentionParser,
EmbeddedWebsocketUrlParser,
EmbeddedUrlParser,
- EmbeddedHashtagParser
+ EmbeddedHashtagParser,
+ EmbeddedEmojiParser
])
+
+ // Create emoji map for quick lookup
+ const emojiMap = new Map
()
+ emojis?.forEach((emoji) => {
+ emojiMap.set(emoji.shortcode, emoji)
+ })
+
return nodes.map((node, index) => {
if (node.type === 'url') {
return
@@ -46,9 +65,15 @@ export default function ProfileAbout({ about, className }: { about?: string; cla
if (node.type === 'mention') {
return
}
+ if (node.type === 'emoji') {
+ const shortcode = node.data.split(':')[1]
+ const emoji = emojiMap.get(shortcode)
+ if (!emoji) return node.data
+ return
+ }
return node.data
})
- }, [about, translatedAbout])
+ }, [about, translatedAbout, emojis])
const handleTranslate = async () => {
if (translating || translatedAbout) return
diff --git a/src/components/ProfileCard/index.tsx b/src/components/ProfileCard/index.tsx
index d167fb6..96e1f16 100644
--- a/src/components/ProfileCard/index.tsx
+++ b/src/components/ProfileCard/index.tsx
@@ -9,7 +9,7 @@ import { SimpleUserAvatar } from '../UserAvatar'
export default function ProfileCard({ userId }: { userId: string }) {
const pubkey = useMemo(() => userIdToPubkey(userId), [userId])
const { profile } = useFetchProfile(userId)
- const { username, about } = profile || {}
+ const { username, about, emojis } = profile || {}
return (
@@ -24,6 +24,7 @@ export default function ProfileCard({ userId }: { userId: string }) {
{about && (
)}
diff --git a/src/components/TextWithEmojis/index.tsx b/src/components/TextWithEmojis/index.tsx
new file mode 100644
index 0000000..1a1a673
--- /dev/null
+++ b/src/components/TextWithEmojis/index.tsx
@@ -0,0 +1,55 @@
+import { EmbeddedEmojiParser, parseContent } from '@/lib/content-parser'
+import { TEmoji } from '@/types'
+import { useMemo } from 'react'
+import Emoji from '../Emoji'
+
+/**
+ * Component that renders text with custom emojis replaced by emoji images
+ * According to NIP-30, custom emojis are defined in emoji tags and referenced as :shortcode: in text
+ */
+export default function TextWithEmojis({
+ text,
+ emojis,
+ className,
+ emojiClassName
+}: {
+ text: string
+ emojis?: TEmoji[]
+ className?: string
+ emojiClassName?: string
+}) {
+ const nodes = useMemo(() => {
+ if (!emojis || emojis.length === 0) {
+ return [{ type: 'text' as const, data: text }]
+ }
+
+ // Use the existing content parser infrastructure
+ return parseContent(text, [EmbeddedEmojiParser])
+ }, [text, emojis])
+
+ // Create emoji map for quick lookup
+ const emojiMap = useMemo(() => {
+ const map = new Map
()
+ emojis?.forEach((emoji) => {
+ map.set(emoji.shortcode, emoji)
+ })
+ return map
+ }, [emojis])
+
+ return (
+
+ {nodes.map((node, index) => {
+ if (node.type === 'text') {
+ return node.data
+ }
+ if (node.type === 'emoji') {
+ const shortcode = node.data.split(':')[1]
+ const emoji = emojiMap.get(shortcode)
+ if (!emoji) return node.data
+ return
+ }
+ return null
+ })}
+
+ )
+}
diff --git a/src/components/Username/index.tsx b/src/components/Username/index.tsx
index 6213c20..57161e2 100644
--- a/src/components/Username/index.tsx
+++ b/src/components/Username/index.tsx
@@ -5,6 +5,7 @@ import { toProfile } from '@/lib/link'
import { cn } from '@/lib/utils'
import { SecondaryPageLink } from '@/PageManager'
import ProfileCard from '../ProfileCard'
+import TextWithEmojis from '../TextWithEmojis'
export default function Username({
userId,
@@ -39,7 +40,7 @@ export default function Username({
onClick={(e) => e.stopPropagation()}
>
{showAt && '@'}
- {profile.username}
+
@@ -73,12 +74,12 @@ export function SimpleUsername({
}
if (!profile) return null
- const { username } = profile
+ const { username, emojis } = profile
return (
{showAt && '@'}
- {username}
+
)
}
diff --git a/src/lib/event-metadata.ts b/src/lib/event-metadata.ts
index 35905e5..e667bc7 100644
--- a/src/lib/event-metadata.ts
+++ b/src/lib/event-metadata.ts
@@ -5,7 +5,7 @@ import { buildATag } from './draft-event'
import { getReplaceableEventIdentifier } from './event'
import { getAmountFromInvoice, getLightningAddressFromProfile } from './lightning'
import { formatPubkey, pubkeyToNpub } from './pubkey'
-import { generateBech32IdFromETag, tagNameEquals } from './tag'
+import { getEmojiInfosFromEmojiTags, generateBech32IdFromETag, tagNameEquals } from './tag'
import { isWebsocketUrl, normalizeHttpUrl, normalizeUrl } from './url'
import { isTorBrowser } from './utils'
@@ -54,6 +54,10 @@ export function getProfileFromEvent(event: Event) {
profileObj.display_name?.trim() ||
profileObj.name?.trim() ||
profileObj.nip05?.split('@')[0]?.trim()
+
+ // Extract emojis from emoji tags according to NIP-30
+ const emojis = getEmojiInfosFromEmojiTags(event.tags)
+
return {
pubkey: event.pubkey,
npub: pubkeyToNpub(event.pubkey) ?? '',
@@ -67,7 +71,8 @@ export function getProfileFromEvent(event: Event) {
lud06: profileObj.lud06,
lud16: profileObj.lud16,
lightningAddress: getLightningAddressFromProfile(profileObj),
- created_at: event.created_at
+ created_at: event.created_at,
+ emojis: emojis.length > 0 ? emojis : undefined
}
} catch (err) {
console.error(event.content, err)
diff --git a/src/types/index.d.ts b/src/types/index.d.ts
index 3df6f8a..bef490e 100644
--- a/src/types/index.d.ts
+++ b/src/types/index.d.ts
@@ -22,6 +22,7 @@ export type TProfile = {
lud16?: string
lightningAddress?: string
created_at?: number
+ emojis?: TEmoji[]
}
export type TMailboxRelayScope = 'read' | 'write' | 'both'
export type TMailboxRelay = {