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,