diff --git a/src/components/ContentPreview/ReactionPreview.tsx b/src/components/ContentPreview/ReactionPreview.tsx
new file mode 100644
index 0000000..7f7479a
--- /dev/null
+++ b/src/components/ContentPreview/ReactionPreview.tsx
@@ -0,0 +1,50 @@
+import Image from '@/components/Image'
+import { cn } from '@/lib/utils'
+import { Heart } from 'lucide-react'
+import { Event } from 'nostr-tools'
+import { useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+
+export default function ReactionPreview({
+ event,
+ className
+}: {
+ event: Event
+ className?: string
+}) {
+ const { t } = useTranslation()
+
+ const reaction = useMemo(() => {
+ if (!event.content || event.content === '+') {
+ return
+ }
+
+ const emojiName = /^:([^:]+):$/.exec(event.content)?.[1]
+ if (emojiName) {
+ const emojiTag = event.tags.find((tag) => tag[0] === 'emoji' && tag[1] === emojiName)
+ const emojiUrl = emojiTag?.[2]
+ if (emojiUrl) {
+ return (
+ }
+ />
+ )
+ }
+ }
+ if (event.content.length > 4) {
+ return
+ }
+ return {event.content}
+ }, [event])
+
+ return (
+
[
diff --git a/src/components/Note/Reaction.tsx b/src/components/Note/Reaction.tsx
new file mode 100644
index 0000000..003c731
--- /dev/null
+++ b/src/components/Note/Reaction.tsx
@@ -0,0 +1,33 @@
+import Emoji from '@/components/Emoji'
+import { getEmojiInfosFromEmojiTags } from '@/lib/tag'
+import { Event } from 'nostr-tools'
+import { useMemo } from 'react'
+
+export default function Reaction({
+ event,
+ className
+}: {
+ event: Event
+ className?: string
+}) {
+ const emoji = useMemo(() => {
+ const content = event.content
+ if (!content || content === '+') return '+'
+
+ const emojiName = /^:([^:]+):$/.exec(content)?.[1]
+ if (emojiName) {
+ const emojiInfos = getEmojiInfosFromEmojiTags(event.tags)
+ const emojiInfo = emojiInfos.find((e) => e.shortcode === emojiName)
+ if (emojiInfo) return emojiInfo
+ }
+
+ if (content.length <= 4) return content
+ return '+'
+ }, [event])
+
+ return (
+
+
+
+ )
+}
diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx
index 5dd1ced..0d53b7a 100644
--- a/src/components/Note/index.tsx
+++ b/src/components/Note/index.tsx
@@ -2,11 +2,13 @@ import { useSecondaryPage } from '@/PageManager'
import { ExtendedKind, NSFW_DISPLAY_POLICY, SUPPORTED_KINDS } from '@/constants'
import { getParentStuff, isNsfwEvent } from '@/lib/event'
import { toExternalContent, toNote } from '@/lib/link'
+import { generateBech32IdFromATag, generateBech32IdFromETag, tagNameEquals } from '@/lib/tag'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/providers/MuteListProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Event, kinds } from 'nostr-tools'
import { useMemo, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import AudioPlayer from '../AudioPlayer'
import ClientTag from '../ClientTag'
import Content from '../Content'
@@ -32,6 +34,7 @@ import MutedNote from './MutedNote'
import NsfwNote from './NsfwNote'
import PictureNote from './PictureNote'
import Poll from './Poll'
+import Reaction from './Reaction'
import RelayReview from './RelayReview'
import UnknownNote from './UnknownNote'
import VideoNote from './VideoNote'
@@ -51,11 +54,21 @@ export default function Note({
hideParentNotePreview?: boolean
showFull?: boolean
}) {
+ const { t } = useTranslation()
const { push } = useSecondaryPage()
const { isSmallScreen } = useScreenSize()
const { parentEventId, parentExternalContent } = useMemo(() => {
return getParentStuff(event)
}, [event])
+ const reactionTargetEventId = useMemo(() => {
+ if (event.kind !== kinds.Reaction && event.kind !== ExtendedKind.EXTERNAL_CONTENT_REACTION) {
+ return undefined
+ }
+ const aTag = event.tags.findLast(tagNameEquals('a'))
+ if (aTag) return generateBech32IdFromATag(aTag)
+ const eTag = event.tags.findLast(tagNameEquals('e'))
+ return eTag ? generateBech32IdFromETag(eTag) : undefined
+ }, [event])
const { nsfwDisplayPolicy } = useContentPolicy()
const [showNsfw, setShowNsfw] = useState(false)
const { mutePubkeySet } = useMuteList()
@@ -117,6 +130,8 @@ export default function Note({
content =
} else if (event.kind === ExtendedKind.FOLLOW_PACK) {
content =
+ } else if (event.kind === kinds.Reaction) {
+ content =
} else {
content =
}
@@ -170,6 +185,17 @@ export default function Note({
}}
/>
)}
+ {reactionTargetEventId && (
+
{
+ e.stopPropagation()
+ push(toNote(reactionTargetEventId))
+ }}
+ />
+ )}
{content}
)
diff --git a/src/components/ParentNotePreview/index.tsx b/src/components/ParentNotePreview/index.tsx
index 2466cef..c9699e7 100644
--- a/src/components/ParentNotePreview/index.tsx
+++ b/src/components/ParentNotePreview/index.tsx
@@ -9,15 +9,18 @@ export default function ParentNotePreview({
eventId,
externalContent,
className,
- onClick
+ onClick,
+ label
}: {
eventId?: string
externalContent?: string
className?: string
onClick?: React.MouseEventHandler | undefined
+ label?: string
}) {
const { t } = useTranslation()
const { event, isFetching } = useFetchEvent(eventId)
+ const displayLabel = label ?? t('reply to')
if (externalContent) {
return (
@@ -28,7 +31,7 @@ export default function ParentNotePreview({
)}
onClick={onClick}
>
- {t('reply to')}
+ {displayLabel}
{externalContent}
)
@@ -46,7 +49,7 @@ export default function ParentNotePreview({
className
)}
>
- {t('reply to')}
+ {displayLabel}
@@ -64,7 +67,7 @@ export default function ParentNotePreview({
)}
onClick={event ? onClick : undefined}
>
-
{t('reply to')}
+
{displayLabel}
{event &&
}
diff --git a/src/components/ReactionList/index.tsx b/src/components/ReactionList/index.tsx
index d025907..ee36cad 100644
--- a/src/components/ReactionList/index.tsx
+++ b/src/components/ReactionList/index.tsx
@@ -2,7 +2,7 @@ import { useSecondaryPage } from '@/PageManager'
import { SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
import { useStuff } from '@/hooks/useStuff'
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
-import { toProfile } from '@/lib/link'
+import { toNote } from '@/lib/link'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
import { TEmoji } from '@/types'
@@ -27,6 +27,7 @@ export default function ReactionList({ stuff }: { stuff: Event | string }) {
const [filteredLikes, setFilteredLikes] = useState<
Array<{
id: string
+ eventId: string
pubkey: string
emoji: string | TEmoji
created_at: number
@@ -38,6 +39,7 @@ export default function ReactionList({ stuff }: { stuff: Event | string }) {
const likes = noteStats?.likes ?? []
const filtered: {
id: string
+ eventId: string
pubkey: string
created_at: number
emoji: string | TEmoji
@@ -81,7 +83,7 @@ export default function ReactionList({ stuff }: { stuff: Event | string }) {
push(toProfile(like.pubkey))}
+ onClick={() => push(toNote(like.eventId))}
>
- likes: { id: string; pubkey: string; created_at: number; emoji: TEmoji | string }[]
+ likes: {
+ id: string
+ eventId: string
+ pubkey: string
+ created_at: number
+ emoji: TEmoji | string
+ }[]
repostPubkeySet: Set
reposts: { id: string; pubkey: string; created_at: number }[]
zapPrSet: Set
@@ -269,7 +280,13 @@ class StuffStatsService {
}
likeIdSet.add(evt.id)
- likes.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at, emoji })
+ likes.push({
+ id: evt.id,
+ eventId: getNoteBech32Id(evt),
+ pubkey: evt.pubkey,
+ created_at: evt.created_at,
+ emoji
+ })
this.stuffStatsMap.set(targetEventKey, { ...old, likeIdSet, likes })
return targetEventKey
}
@@ -298,7 +315,13 @@ class StuffStatsService {
}
likeIdSet.add(evt.id)
- likes.push({ id: evt.id, pubkey: evt.pubkey, created_at: evt.created_at, emoji })
+ likes.push({
+ id: evt.id,
+ eventId: getNoteBech32Id(evt),
+ pubkey: evt.pubkey,
+ created_at: evt.created_at,
+ emoji
+ })
this.stuffStatsMap.set(target, { ...old, likeIdSet, likes })
return target
}