diff --git a/src/components/ContentPreview/index.tsx b/src/components/ContentPreview/index.tsx index 2639400b..0c4ea7a2 100644 --- a/src/components/ContentPreview/index.tsx +++ b/src/components/ContentPreview/index.tsx @@ -126,5 +126,32 @@ export default function ContentPreview({ return } + if (event.kind === kinds.Reaction) { + const raw = event.content?.trim() ?? '' + const glyph = !raw ? '❤️' : raw.length > 24 ? `${raw.slice(0, 24)}…` : raw + return ( +
+ {glyph} + {t('Notification reaction summary')} +
+ ) + } + + if (event.kind === kinds.Repost) { + return ( +
+ {t('Notification boost summary')} +
+ ) + } + + if (event.kind === ExtendedKind.POLL_RESPONSE) { + return ( +
+ {t('Notification poll vote summary')} +
+ ) + } + return
[{t('Cannot handle event of kind k', { k: event.kind })}]
} diff --git a/src/components/Note/NotificationEventCard.tsx b/src/components/Note/NotificationEventCard.tsx new file mode 100644 index 00000000..c55f9191 --- /dev/null +++ b/src/components/Note/NotificationEventCard.tsx @@ -0,0 +1,74 @@ +import { ExtendedKind } from '@/constants' +import { cn } from '@/lib/utils' +import { Event, kinds } from 'nostr-tools' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +/** + * Compact card for interaction events in notification-style feeds (reactions, boosts, poll votes). + * The surrounding {@link Note} row still shows author + {@link ParentNotePreview} for the target. + */ +export default function NotificationEventCard({ event, className }: { event: Event; className?: string }) { + const { t } = useTranslation() + + const reactionDisplay = useMemo(() => { + if (event.kind !== kinds.Reaction) return null + const raw = event.content?.trim() ?? '' + if (!raw) return '❤️' + if (raw.length > 64) return `${raw.slice(0, 64)}…` + return raw + }, [event.content, event.kind]) + + if (event.kind === kinds.Reaction) { + return ( +
+
+ + {reactionDisplay} + +

{t('Notification reaction summary')}

+
+
+ ) + } + + if (event.kind === kinds.Repost) { + return ( +
+

{t('Notification boost summary')}

+

{t('Notification boost detail')}

+
+ ) + } + + if (event.kind === ExtendedKind.POLL_RESPONSE) { + const n = event.tags.filter((tag) => tag[0] === 'response' && tag[1]).length + return ( +
+

{t('Notification poll vote summary')}

+ {n > 0 ? ( +

+ {t('Notification poll vote options count', { count: n })} +

+ ) : null} +
+ ) + } + + return null +} diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index 9994f12e..f037fd2f 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -41,6 +41,7 @@ import MutedNote from './MutedNote' import NsfwNote from './NsfwNote' import PictureNote from './PictureNote' import Poll from './Poll' +import NotificationEventCard from './NotificationEventCard' import UnknownNote from './UnknownNote' import VideoNote from './VideoNote' import RelayReview from './RelayReview' @@ -129,6 +130,12 @@ export default function Note({ content = setShowMuted(true)} /> } else if (!defaultShowNsfw && isNsfwEvent(event) && !showNsfw) { content = setShowNsfw(true)} /> + } else if ( + event.kind === kinds.Reaction || + event.kind === kinds.Repost || + event.kind === ExtendedKind.POLL_RESPONSE + ) { + content = } else if (event.kind === kinds.Highlights) { // Try to render the Highlight component with error boundary try { diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 5cda8bd1..72236f3b 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -373,6 +373,11 @@ export default { Topics: 'Themen', 'Open in a': 'Öffnen in {{a}}', 'Cannot handle event of kind k': 'Ereignis des Typs {{k}} kann nicht verarbeitet werden', + 'Notification reaction summary': 'Hat auf die Notiz darüber reagiert.', + 'Notification boost summary': 'Hat diese Notiz geboostet', + 'Notification boost detail': 'Die Vorschau darüber ist der Originalbeitrag.', + 'Notification poll vote summary': 'Hat an der Umfrage darüber teilgenommen.', + 'Notification poll vote options count': '{{count}} Option(en) gewählt', 'Jumble Imwald synthetic event': 'Jumble Imwald – synthetisches Ereignis', '+ Add a URL to this list': 'URL zur Liste hinzufügen', 'Add a web URL': 'Web-URL hinzufügen', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 0622bcf6..4703d876 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -366,6 +366,11 @@ export default { Topics: 'Topics', 'Open in a': 'Open in {{a}}', 'Cannot handle event of kind k': 'Cannot handle event of kind {{k}}', + 'Notification reaction summary': 'Reacted to the note above.', + 'Notification boost summary': 'Boosted this note', + 'Notification boost detail': 'The preview above is the original post.', + 'Notification poll vote summary': 'Voted on the poll above.', + 'Notification poll vote options count': '{{count}} option(s) selected', 'Jumble Imwald synthetic event': 'Jumble Imwald synthetic event', '+ Add a URL to this list': 'Add a URL to this list', 'Add a web URL': 'Add a web URL', diff --git a/src/lib/event.ts b/src/lib/event.ts index 5501edd0..e754a2c8 100644 --- a/src/lib/event.ts +++ b/src/lib/event.ts @@ -10,6 +10,7 @@ import { hexPubkeysEqual, normalizeHexPubkey } from './pubkey' import { generateBech32IdFromATag, generateBech32IdFromETag, + getFirstHexEventIdFromETags, getImetaInfoFromImetaTag, tagNameEquals } from './tag' @@ -73,6 +74,20 @@ export function isMentioningMutedUsers(event: Event, mutePubkeySet: Set) export function getParentETag(event?: Event) { if (!event) return undefined + // NIP-25 reactions, NIP-18 reposts, poll responses: first hex `e` / `E` references the target note. + if ( + event.kind === kinds.Reaction || + event.kind === kinds.Repost || + event.kind === ExtendedKind.POLL_RESPONSE + ) { + const firstId = getFirstHexEventIdFromETags(event.tags) + if (!firstId) return undefined + return ( + event.tags.find((t) => t[0] === 'e' && t[1] === firstId) ?? + event.tags.find((t) => t[0] === 'E' && t[1] === firstId) + ) + } + if (event.kind === ExtendedKind.COMMENT || event.kind === ExtendedKind.VOICE_COMMENT) { return event.tags.find(tagNameEquals('e')) ?? event.tags.find(tagNameEquals('E')) } diff --git a/src/lib/note-renderable-kinds.ts b/src/lib/note-renderable-kinds.ts index 3e153d1a..8fff0019 100644 --- a/src/lib/note-renderable-kinds.ts +++ b/src/lib/note-renderable-kinds.ts @@ -4,6 +4,8 @@ import { kinds } from 'nostr-tools' /** Kinds the main `Note` component renders with a dedicated UI (not `UnknownNote`). */ const RENDERABLE_NOTE_KINDS = new Set([ ...SUPPORTED_KINDS, + kinds.Reaction, + ExtendedKind.POLL_RESPONSE, kinds.CommunityDefinition, kinds.LiveEvent, ExtendedKind.GROUP_METADATA,