Browse Source

have cards for notification event kinds

imwald
Silberengel 1 month ago
parent
commit
894edc296e
  1. 27
      src/components/ContentPreview/index.tsx
  2. 74
      src/components/Note/NotificationEventCard.tsx
  3. 7
      src/components/Note/index.tsx
  4. 5
      src/i18n/locales/de.ts
  5. 5
      src/i18n/locales/en.ts
  6. 15
      src/lib/event.ts
  7. 2
      src/lib/note-renderable-kinds.ts

27
src/components/ContentPreview/index.tsx

@ -126,5 +126,32 @@ export default function ContentPreview({
return <FollowPackPreview event={event} className={className} /> return <FollowPackPreview event={event} className={className} />
} }
if (event.kind === kinds.Reaction) {
const raw = event.content?.trim() ?? ''
const glyph = !raw ? '❤' : raw.length > 24 ? `${raw.slice(0, 24)}` : raw
return (
<div className={cn('pointer-events-none text-sm text-muted-foreground', className)}>
<span className="mr-1.5">{glyph}</span>
{t('Notification reaction summary')}
</div>
)
}
if (event.kind === kinds.Repost) {
return (
<div className={cn('pointer-events-none text-sm text-muted-foreground', className)}>
{t('Notification boost summary')}
</div>
)
}
if (event.kind === ExtendedKind.POLL_RESPONSE) {
return (
<div className={cn('pointer-events-none text-sm text-muted-foreground', className)}>
{t('Notification poll vote summary')}
</div>
)
}
return <div className={className}>[{t('Cannot handle event of kind k', { k: event.kind })}]</div> return <div className={className}>[{t('Cannot handle event of kind k', { k: event.kind })}]</div>
} }

74
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 (
<div
className={cn(
'rounded-lg border border-border bg-card p-4 text-card-foreground shadow-sm',
className
)}
>
<div className="flex flex-wrap items-center gap-3">
<span className="text-3xl leading-none select-none" aria-hidden>
{reactionDisplay}
</span>
<p className="text-sm text-muted-foreground">{t('Notification reaction summary')}</p>
</div>
</div>
)
}
if (event.kind === kinds.Repost) {
return (
<div
className={cn(
'rounded-lg border border-border bg-card px-4 py-3 text-card-foreground shadow-sm',
className
)}
>
<p className="text-sm font-medium">{t('Notification boost summary')}</p>
<p className="mt-1 text-xs text-muted-foreground">{t('Notification boost detail')}</p>
</div>
)
}
if (event.kind === ExtendedKind.POLL_RESPONSE) {
const n = event.tags.filter((tag) => tag[0] === 'response' && tag[1]).length
return (
<div
className={cn(
'rounded-lg border border-border bg-card px-4 py-3 text-card-foreground shadow-sm',
className
)}
>
<p className="text-sm font-medium">{t('Notification poll vote summary')}</p>
{n > 0 ? (
<p className="mt-1 text-xs text-muted-foreground">
{t('Notification poll vote options count', { count: n })}
</p>
) : null}
</div>
)
}
return null
}

7
src/components/Note/index.tsx

@ -41,6 +41,7 @@ import MutedNote from './MutedNote'
import NsfwNote from './NsfwNote' import NsfwNote from './NsfwNote'
import PictureNote from './PictureNote' import PictureNote from './PictureNote'
import Poll from './Poll' import Poll from './Poll'
import NotificationEventCard from './NotificationEventCard'
import UnknownNote from './UnknownNote' import UnknownNote from './UnknownNote'
import VideoNote from './VideoNote' import VideoNote from './VideoNote'
import RelayReview from './RelayReview' import RelayReview from './RelayReview'
@ -129,6 +130,12 @@ export default function Note({
content = <MutedNote show={() => setShowMuted(true)} /> content = <MutedNote show={() => setShowMuted(true)} />
} else if (!defaultShowNsfw && isNsfwEvent(event) && !showNsfw) { } else if (!defaultShowNsfw && isNsfwEvent(event) && !showNsfw) {
content = <NsfwNote show={() => setShowNsfw(true)} /> content = <NsfwNote show={() => setShowNsfw(true)} />
} else if (
event.kind === kinds.Reaction ||
event.kind === kinds.Repost ||
event.kind === ExtendedKind.POLL_RESPONSE
) {
content = <NotificationEventCard className="mt-2" event={event} />
} else if (event.kind === kinds.Highlights) { } else if (event.kind === kinds.Highlights) {
// Try to render the Highlight component with error boundary // Try to render the Highlight component with error boundary
try { try {

5
src/i18n/locales/de.ts

@ -373,6 +373,11 @@ export default {
Topics: 'Themen', Topics: 'Themen',
'Open in a': 'Öffnen in {{a}}', 'Open in a': 'Öffnen in {{a}}',
'Cannot handle event of kind k': 'Ereignis des Typs {{k}} kann nicht verarbeitet werden', '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', 'Jumble Imwald synthetic event': 'Jumble Imwald – synthetisches Ereignis',
'+ Add a URL to this list': 'URL zur Liste hinzufügen', '+ Add a URL to this list': 'URL zur Liste hinzufügen',
'Add a web URL': 'Web-URL hinzufügen', 'Add a web URL': 'Web-URL hinzufügen',

5
src/i18n/locales/en.ts

@ -366,6 +366,11 @@ export default {
Topics: 'Topics', Topics: 'Topics',
'Open in a': 'Open in {{a}}', 'Open in a': 'Open in {{a}}',
'Cannot handle event of kind k': 'Cannot handle event of kind {{k}}', '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', 'Jumble Imwald synthetic event': 'Jumble Imwald synthetic event',
'+ Add a URL to this list': 'Add a URL to this list', '+ Add a URL to this list': 'Add a URL to this list',
'Add a web URL': 'Add a web URL', 'Add a web URL': 'Add a web URL',

15
src/lib/event.ts

@ -10,6 +10,7 @@ import { hexPubkeysEqual, normalizeHexPubkey } from './pubkey'
import { import {
generateBech32IdFromATag, generateBech32IdFromATag,
generateBech32IdFromETag, generateBech32IdFromETag,
getFirstHexEventIdFromETags,
getImetaInfoFromImetaTag, getImetaInfoFromImetaTag,
tagNameEquals tagNameEquals
} from './tag' } from './tag'
@ -73,6 +74,20 @@ export function isMentioningMutedUsers(event: Event, mutePubkeySet: Set<string>)
export function getParentETag(event?: Event) { export function getParentETag(event?: Event) {
if (!event) return undefined 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) { if (event.kind === ExtendedKind.COMMENT || event.kind === ExtendedKind.VOICE_COMMENT) {
return event.tags.find(tagNameEquals('e')) ?? event.tags.find(tagNameEquals('E')) return event.tags.find(tagNameEquals('e')) ?? event.tags.find(tagNameEquals('E'))
} }

2
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`). */ /** Kinds the main `Note` component renders with a dedicated UI (not `UnknownNote`). */
const RENDERABLE_NOTE_KINDS = new Set<number>([ const RENDERABLE_NOTE_KINDS = new Set<number>([
...SUPPORTED_KINDS, ...SUPPORTED_KINDS,
kinds.Reaction,
ExtendedKind.POLL_RESPONSE,
kinds.CommunityDefinition, kinds.CommunityDefinition,
kinds.LiveEvent, kinds.LiveEvent,
ExtendedKind.GROUP_METADATA, ExtendedKind.GROUP_METADATA,

Loading…
Cancel
Save