diff --git a/src/components/Content/index.tsx b/src/components/Content/index.tsx
index b7b7be4..1e9921c 100644
--- a/src/components/Content/index.tsx
+++ b/src/components/Content/index.tsx
@@ -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
}
+ if (node.type === 'emoji') {
+ const shortcode = node.data.split(':')[1]
+ const emoji = emojiInfos.find((e) => e.shortcode === shortcode)
+ if (!emoji) return node.data
+ return
+ }
return null
})}
{lastNormalUrl && (
diff --git a/src/components/ContentPreview/index.tsx b/src/components/ContentPreview/index.tsx
index ee04ca0..3cf5acf 100644
--- a/src/components/ContentPreview/index.tsx
+++ b/src/components/ContentPreview/index.tsx
@@ -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 (
{nodes.map((node, index) => {
@@ -48,6 +54,12 @@ export default function ContentPreview({
if (node.type === 'mention') {
return
}
+ if (node.type === 'emoji') {
+ const shortcode = node.data.split(':')[1]
+ const emoji = emojiInfos.find((e) => e.shortcode === shortcode)
+ if (!emoji) return node.data
+ return
+ }
})}
)
diff --git a/src/components/Emoji/index.tsx b/src/components/Emoji/index.tsx
new file mode 100644
index 0000000..c201635
--- /dev/null
+++ b/src/components/Emoji/index.tsx
@@ -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 & {
+ className?: string
+ emoji: TEmoji
+}) {
+ const [hasError, setHasError] = useState(false)
+
+ if (hasError) return `:${emoji.shortcode}:`
+
+ return (
+
{
+ setHasError(false)
+ }}
+ onError={() => {
+ setHasError(true)
+ }}
+ />
+ )
+}
diff --git a/src/components/NotificationList/NotificationItem/ReactionNotification.tsx b/src/components/NotificationList/NotificationItem/ReactionNotification.tsx
index 9caf902..fc94260 100644
--- a/src/components/NotificationList/NotificationItem/ReactionNotification.tsx
+++ b/src/components/NotificationList/NotificationItem/ReactionNotification.tsx
@@ -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 (
{
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 (
@@ -48,6 +53,12 @@ const PictureContent = memo(({ event, className }: { event: Event; className?: s
if (node.type === 'mention') {
return
}
+ if (node.type === 'emoji') {
+ const shortcode = node.data.split(':')[1]
+ const emoji = emojiInfos.find((e) => e.shortcode === shortcode)
+ if (!emoji) return node.data
+ return
+ }
})}
diff --git a/src/components/WebPreview/index.tsx b/src/components/WebPreview/index.tsx
index cc36da6..e96aaf9 100644
--- a/src/components/WebPreview/index.tsx
+++ b/src/components/WebPreview/index.tsx
@@ -56,7 +56,7 @@ export default function WebPreview({
{image && (
)}
diff --git a/src/lib/content-parser.ts b/src/lib/content-parser.ts
index 7c03b88..caee276 100644
--- a/src/lib/content-parser.ts
+++ b/src/lib/content-parser.ts
@@ -9,6 +9,7 @@ export type TEmbeddedNodeType =
| 'hashtag'
| 'websocket-url'
| 'url'
+ | 'emoji'
export type TEmbeddedNode =
| {
@@ -64,6 +65,11 @@ export const EmbeddedNormalUrlParser: TContentParser = {
regex: /https?:\/\/[\w\p{L}\p{N}\p{M}&.-/?=#\-@%+_:!~*]+/gu
}
+export const EmbeddedEmojiParser: TContentParser = {
+ type: 'emoji',
+ regex: /:[a-zA-Z0-9_]+:/g
+}
+
export function parseContent(content: string, parsers: TContentParser[]) {
let nodes: TEmbeddedNode[] = [{ type: 'text', data: content.trim() }]
diff --git a/src/lib/event.ts b/src/lib/event.ts
index 562159d..c312bd9 100644
--- a/src/lib/event.ts
+++ b/src/lib/event.ts
@@ -1,6 +1,6 @@
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
import client from '@/services/client.service'
-import { TImageInfo, TRelayList, TRelaySet } from '@/types'
+import { TEmoji, TImageInfo, TRelayList, TRelaySet } from '@/types'
import { LRUCache } from 'lru-cache'
import { Event, kinds, nip19 } from 'nostr-tools'
import { getAmountFromInvoice, getLightningAddressFromProfile } from './lightning'
@@ -505,3 +505,12 @@ export function getLatestEvent(events: Event[]) {
export function getReplaceableEventIdentifier(event: Event) {
return event.tags.find(tagNameEquals('d'))?.[1] ?? ''
}
+
+export function extractEmojiInfosFromTags(tags: string[][] = []) {
+ return tags
+ .map((tag) => {
+ if (tag.length < 3 || tag[0] !== 'emoji') return null
+ return { shortcode: tag[1], url: tag[2] }
+ })
+ .filter(Boolean) as TEmoji[]
+}
diff --git a/src/lib/tag.ts b/src/lib/tag.ts
index 86bbd8e..55f8893 100644
--- a/src/lib/tag.ts
+++ b/src/lib/tag.ts
@@ -66,11 +66,6 @@ export function extractPubkeysFromEventTags(tags: string[][]) {
)
}
-export function extractEmojiFromEventTags(emojiName: string, tags: string[][]) {
- const emojiTag = tags.find((tag) => tag[0] === 'emoji' && tag[1] === emojiName)
- return emojiTag?.[2]
-}
-
export function isSameTag(tag1: string[], tag2: string[]) {
if (tag1.length !== tag2.length) return false
for (let i = 0; i < tag1.length; i++) {
diff --git a/src/types.ts b/src/types.ts
index fdc8b9a..43a8ae9 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -115,3 +115,8 @@ export type TNip66RelayInfo = TRelayInfo & {
relayType?: string
countryCode?: string
}
+
+export type TEmoji = {
+ shortcode: string
+ url: string
+}