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 ( + {emojiName}} + /> + ) + } + } + if (event.content.length > 4) { + return + } + return {event.content} + }, [event]) + + return ( +
+ [{t('Reaction')}] + {reaction} +
+ ) +} diff --git a/src/components/ContentPreview/index.tsx b/src/components/ContentPreview/index.tsx index 5cf0759..576bdca 100644 --- a/src/components/ContentPreview/index.tsx +++ b/src/components/ContentPreview/index.tsx @@ -16,6 +16,7 @@ import LongFormArticlePreview from './LongFormArticlePreview' import NormalContentPreview from './NormalContentPreview' import PictureNotePreview from './PictureNotePreview' import PollPreview from './PollPreview' +import ReactionPreview from './ReactionPreview' import VideoNotePreview from './VideoNotePreview' export default function ContentPreview({ @@ -110,6 +111,10 @@ export default function ContentPreview({ return } + if (event.kind === kinds.Reaction || event.kind === ExtendedKind.EXTERNAL_CONTENT_REACTION) { + return + } + 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 }