From 319ae5a0ba2f33cd82c1d0da1d39322503526fa5 Mon Sep 17 00:00:00 2001 From: codytseng Date: Thu, 17 Apr 2025 17:09:22 +0800 Subject: [PATCH] feat: embedded emoji --- src/components/Content/index.tsx | 15 ++++++++-- src/components/ContentPreview/index.tsx | 14 ++++++++- src/components/Emoji/index.tsx | 29 +++++++++++++++++++ .../NotificationItem/ReactionNotification.tsx | 5 ++-- src/components/PictureContent/index.tsx | 15 ++++++++-- src/components/WebPreview/index.tsx | 2 +- src/lib/content-parser.ts | 6 ++++ src/lib/event.ts | 11 ++++++- src/lib/tag.ts | 5 ---- src/types.ts | 5 ++++ 10 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 src/components/Emoji/index.tsx 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 ( + {emoji.shortcode} { + 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 +}