feat: distinguish between mention and reply notifications

This commit is contained in:
codytseng
2025-07-31 23:07:57 +08:00
parent 629ad3f7cd
commit 5107ce4b77
5 changed files with 56 additions and 50 deletions

View File

@@ -1,13 +1,16 @@
import { getEmbeddedPubkeys } from '@/lib/event'
import { toNote } from '@/lib/link' import { toNote } from '@/lib/link'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useSecondaryPage } from '@/PageManager' import { useSecondaryPage } from '@/PageManager'
import { MessageCircle } from 'lucide-react' import { useNostr } from '@/providers/NostrProvider'
import { AtSign, MessageCircle } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import ContentPreview from '../../ContentPreview' import ContentPreview from '../../ContentPreview'
import { FormattedTimestamp } from '../../FormattedTimestamp' import { FormattedTimestamp } from '../../FormattedTimestamp'
import UserAvatar from '../../UserAvatar' import UserAvatar from '../../UserAvatar'
export function CommentNotification({ export function MentionNotification({
notification, notification,
isNew = false isNew = false
}: { }: {
@@ -15,6 +18,12 @@ export function CommentNotification({
isNew?: boolean isNew?: boolean
}) { }) {
const { push } = useSecondaryPage() const { push } = useSecondaryPage()
const { pubkey } = useNostr()
const isMention = useMemo(() => {
if (!pubkey) return false
const mentions = getEmbeddedPubkeys(notification)
return mentions.includes(pubkey)
}, [pubkey, notification])
return ( return (
<div <div
@@ -22,7 +31,11 @@ export function CommentNotification({
onClick={() => push(toNote(notification))} onClick={() => push(toNote(notification))}
> >
<UserAvatar userId={notification.pubkey} size="small" /> <UserAvatar userId={notification.pubkey} size="small" />
{isMention ? (
<AtSign size={24} className="text-pink-400" />
) : (
<MessageCircle size={24} className="text-blue-400" /> <MessageCircle size={24} className="text-blue-400" />
)}
<ContentPreview <ContentPreview
className={cn('truncate flex-1 w-0', isNew ? 'font-semibold' : 'text-muted-foreground')} className={cn('truncate flex-1 w-0', isNew ? 'font-semibold' : 'text-muted-foreground')}
event={notification} event={notification}

View File

@@ -1,34 +0,0 @@
import { toNote } from '@/lib/link'
import { cn } from '@/lib/utils'
import { useSecondaryPage } from '@/PageManager'
import { MessageCircle } from 'lucide-react'
import { Event } from 'nostr-tools'
import ContentPreview from '../../ContentPreview'
import { FormattedTimestamp } from '../../FormattedTimestamp'
import UserAvatar from '../../UserAvatar'
export function ReplyNotification({
notification,
isNew = false
}: {
notification: Event
isNew?: boolean
}) {
const { push } = useSecondaryPage()
return (
<div
className="flex gap-2 items-center cursor-pointer py-2"
onClick={() => push(toNote(notification))}
>
<UserAvatar userId={notification.pubkey} size="small" />
<MessageCircle size={24} className="text-blue-400" />
<ContentPreview
className={cn('truncate flex-1 w-0', isNew ? 'font-semibold' : 'text-muted-foreground')}
event={notification}
/>
<div className="text-muted-foreground">
<FormattedTimestamp timestamp={notification.created_at} short />
</div>
</div>
)
}

View File

@@ -1,10 +1,9 @@
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { CommentNotification } from './CommentNotification' import { MentionNotification } from './MentionNotification'
import { PollResponseNotification } from './PollResponseNotification' import { PollResponseNotification } from './PollResponseNotification'
import { ReactionNotification } from './ReactionNotification' import { ReactionNotification } from './ReactionNotification'
import { ReplyNotification } from './ReplyNotification'
import { RepostNotification } from './RepostNotification' import { RepostNotification } from './RepostNotification'
import { ZapNotification } from './ZapNotification' import { ZapNotification } from './ZapNotification'
@@ -22,8 +21,13 @@ export function NotificationItem({
if (notification.kind === kinds.Reaction) { if (notification.kind === kinds.Reaction) {
return <ReactionNotification notification={notification} isNew={isNew} /> return <ReactionNotification notification={notification} isNew={isNew} />
} }
if (notification.kind === kinds.ShortTextNote) { if (
return <ReplyNotification notification={notification} isNew={isNew} /> notification.kind === kinds.ShortTextNote ||
notification.kind === ExtendedKind.COMMENT ||
notification.kind === ExtendedKind.VOICE_COMMENT ||
notification.kind === ExtendedKind.POLL
) {
return <MentionNotification notification={notification} isNew={isNew} />
} }
if (notification.kind === kinds.Repost) { if (notification.kind === kinds.Repost) {
return <RepostNotification notification={notification} isNew={isNew} /> return <RepostNotification notification={notification} isNew={isNew} />
@@ -31,12 +35,6 @@ export function NotificationItem({
if (notification.kind === kinds.Zap) { if (notification.kind === kinds.Zap) {
return <ZapNotification notification={notification} isNew={isNew} /> return <ZapNotification notification={notification} isNew={isNew} />
} }
if (
notification.kind === ExtendedKind.COMMENT ||
notification.kind === ExtendedKind.VOICE_COMMENT
) {
return <CommentNotification notification={notification} isNew={isNew} />
}
if (notification.kind === ExtendedKind.POLL_RESPONSE) { if (notification.kind === ExtendedKind.POLL_RESPONSE) {
return <PollResponseNotification notification={notification} isNew={isNew} /> return <PollResponseNotification notification={notification} isNew={isNew} />
} }

View File

@@ -39,7 +39,12 @@ const NotificationList = forwardRef((_, ref) => {
const filterKinds = useMemo(() => { const filterKinds = useMemo(() => {
switch (notificationType) { switch (notificationType) {
case 'mentions': case 'mentions':
return [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT] return [
kinds.ShortTextNote,
ExtendedKind.COMMENT,
ExtendedKind.VOICE_COMMENT,
ExtendedKind.POLL
]
case 'reactions': case 'reactions':
return [kinds.Reaction, kinds.Repost, ExtendedKind.POLL_RESPONSE] return [kinds.Reaction, kinds.Repost, ExtendedKind.POLL_RESPONSE]
case 'zaps': case 'zaps':
@@ -52,7 +57,8 @@ const NotificationList = forwardRef((_, ref) => {
kinds.Zap, kinds.Zap,
ExtendedKind.COMMENT, ExtendedKind.COMMENT,
ExtendedKind.POLL_RESPONSE, ExtendedKind.POLL_RESPONSE,
ExtendedKind.VOICE_COMMENT ExtendedKind.VOICE_COMMENT,
ExtendedKind.POLL
] ]
} }
}, [notificationType]) }, [notificationType])

View File

@@ -1,4 +1,4 @@
import { ExtendedKind } from '@/constants' import { EMBEDDED_MENTION_REGEX, ExtendedKind } from '@/constants'
import client from '@/services/client.service' import client from '@/services/client.service'
import { TImageInfo } from '@/types' import { TImageInfo } from '@/types'
import { LRUCache } from 'lru-cache' import { LRUCache } from 'lru-cache'
@@ -11,6 +11,7 @@ import {
} from './tag' } from './tag'
const EVENT_EMBEDDED_NOTES_CACHE = new LRUCache<string, string[]>({ max: 10000 }) const EVENT_EMBEDDED_NOTES_CACHE = new LRUCache<string, string[]>({ max: 10000 })
const EVENT_EMBEDDED_PUBKEYS_CACHE = new LRUCache<string, string[]>({ max: 10000 })
const EVENT_IS_REPLY_NOTE_CACHE = new LRUCache<string, boolean>({ max: 10000 }) const EVENT_IS_REPLY_NOTE_CACHE = new LRUCache<string, boolean>({ max: 10000 })
export function isNsfwEvent(event: Event) { export function isNsfwEvent(event: Event) {
@@ -199,6 +200,28 @@ export function getEmbeddedNoteBech32Ids(event: Event) {
return embeddedNoteBech32Ids return embeddedNoteBech32Ids
} }
export function getEmbeddedPubkeys(event: Event) {
const cache = EVENT_EMBEDDED_PUBKEYS_CACHE.get(event.id)
if (cache) return cache
const embeddedPubkeySet = new Set<string>()
;(event.content.match(EMBEDDED_MENTION_REGEX) || []).forEach((mention) => {
try {
const { type, data } = nip19.decode(mention.split(':')[1])
if (type === 'npub') {
embeddedPubkeySet.add(data)
} else if (type === 'nprofile') {
embeddedPubkeySet.add(data.pubkey)
}
} catch {
// ignore
}
})
const embeddedPubkeys = Array.from(embeddedPubkeySet)
EVENT_EMBEDDED_PUBKEYS_CACHE.set(event.id, embeddedPubkeys)
return embeddedPubkeys
}
export function getLatestEvent(events: Event[]) { export function getLatestEvent(events: Event[]) {
return events.sort((a, b) => b.created_at - a.created_at)[0] return events.sort((a, b) => b.created_at - a.created_at)[0]
} }