feat: optimize notifications
This commit is contained in:
parent
47e7a18f2e
commit
2855754648
26 changed files with 520 additions and 234 deletions
|
|
@ -1,14 +1,13 @@
|
|||
import { getEmbeddedPubkeys } from '@/lib/event'
|
||||
import ParentNotePreview from '@/components/ParentNotePreview'
|
||||
import { getEmbeddedPubkeys, getParentBech32Id } from '@/lib/event'
|
||||
import { toNote } from '@/lib/link'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { AtSign, MessageCircle } from 'lucide-react'
|
||||
import { AtSign, MessageCircle, Quote } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
import ContentPreview from '../../ContentPreview'
|
||||
import { FormattedTimestamp } from '../../FormattedTimestamp'
|
||||
import UserAvatar from '../../UserAvatar'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Notification from './Notification'
|
||||
|
||||
export function MentionNotification({
|
||||
notification,
|
||||
|
|
@ -17,6 +16,7 @@ export function MentionNotification({
|
|||
notification: Event
|
||||
isNew?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { push } = useSecondaryPage()
|
||||
const { pubkey } = useNostr()
|
||||
const isMention = useMemo(() => {
|
||||
|
|
@ -24,25 +24,40 @@ export function MentionNotification({
|
|||
const mentions = getEmbeddedPubkeys(notification)
|
||||
return mentions.includes(pubkey)
|
||||
}, [pubkey, notification])
|
||||
const parentEventId = useMemo(() => getParentBech32Id(notification), [notification])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex gap-2 items-center cursor-pointer py-2"
|
||||
onClick={() => push(toNote(notification))}
|
||||
>
|
||||
<UserAvatar userId={notification.pubkey} size="small" />
|
||||
{isMention ? (
|
||||
<AtSign size={24} className="text-pink-400" />
|
||||
) : (
|
||||
<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>
|
||||
<Notification
|
||||
notificationId={notification.id}
|
||||
icon={
|
||||
isMention ? (
|
||||
<AtSign size={24} className="text-pink-400" />
|
||||
) : parentEventId ? (
|
||||
<MessageCircle size={24} className="text-blue-400" />
|
||||
) : (
|
||||
<Quote size={24} className="text-green-400" />
|
||||
)
|
||||
}
|
||||
sender={notification.pubkey}
|
||||
sentAt={notification.created_at}
|
||||
targetEvent={notification}
|
||||
middle={
|
||||
parentEventId && (
|
||||
<ParentNotePreview
|
||||
eventId={parentEventId}
|
||||
className=""
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
push(toNote(parentEventId))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
description={
|
||||
isMention ? t('mentioned you in a note') : parentEventId ? '' : t('quoted your note')
|
||||
}
|
||||
isNew={isNew}
|
||||
showStats
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
import ContentPreview from '@/components/ContentPreview'
|
||||
import { FormattedTimestamp } from '@/components/FormattedTimestamp'
|
||||
import NoteStats from '@/components/NoteStats'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import UserAvatar from '@/components/UserAvatar'
|
||||
import Username from '@/components/Username'
|
||||
import { toNote, toProfile } from '@/lib/link'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useNotification } from '@/providers/NotificationProvider'
|
||||
import { NostrEvent } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function Notification({
|
||||
icon,
|
||||
notificationId,
|
||||
sender,
|
||||
sentAt,
|
||||
description,
|
||||
middle = null,
|
||||
targetEvent,
|
||||
isNew = false,
|
||||
showStats = false
|
||||
}: {
|
||||
icon: React.ReactNode
|
||||
notificationId: string
|
||||
sender: string
|
||||
sentAt: number
|
||||
description: string
|
||||
middle?: React.ReactNode
|
||||
targetEvent?: NostrEvent
|
||||
isNew?: boolean
|
||||
showStats?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { push } = useSecondaryPage()
|
||||
const { pubkey } = useNostr()
|
||||
const { isNotificationRead, markNotificationAsRead } = useNotification()
|
||||
const unread = useMemo(
|
||||
() => isNew && !isNotificationRead(notificationId),
|
||||
[isNew, isNotificationRead, notificationId]
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="clickable flex items-start gap-2 cursor-pointer py-2 px-4 border-b"
|
||||
onClick={() => {
|
||||
markNotificationAsRead(notificationId)
|
||||
if (targetEvent) {
|
||||
push(toNote(targetEvent.id))
|
||||
} else if (pubkey) {
|
||||
push(toProfile(pubkey))
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-2 items-center mt-1.5">
|
||||
{icon}
|
||||
<UserAvatar userId={sender} size="medium" />
|
||||
</div>
|
||||
<div className="flex-1 w-0">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<div className="flex gap-1 items-center">
|
||||
<Username
|
||||
userId={sender}
|
||||
className="flex-1 max-w-fit truncate font-semibold"
|
||||
skeletonClassName="h-4"
|
||||
/>
|
||||
<div className="shrink-0 text-muted-foreground text-sm">{description}</div>
|
||||
</div>
|
||||
{unread && (
|
||||
<button
|
||||
className="m-0.5 size-3 bg-primary rounded-full shrink-0 transition-all hover:ring-4 hover:ring-primary/20"
|
||||
title={t('Mark as read')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
markNotificationAsRead(notificationId)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{middle}
|
||||
{targetEvent && (
|
||||
<ContentPreview
|
||||
className={cn('line-clamp-2', !unread && 'text-muted-foreground')}
|
||||
event={targetEvent}
|
||||
/>
|
||||
)}
|
||||
<FormattedTimestamp timestamp={sentAt} className="shrink-0 text-muted-foreground text-sm" />
|
||||
{showStats && targetEvent && <NoteStats event={targetEvent} className="mt-1" />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function NotificationSkeleton() {
|
||||
return (
|
||||
<div className="flex items-start gap-2 cursor-pointer py-2 px-4">
|
||||
<div className="flex gap-2 items-center mt-1.5">
|
||||
<Skeleton className="w-6 h-6" />
|
||||
<Skeleton className="w-9 h-9 rounded-full" />
|
||||
</div>
|
||||
<div className="flex-1 w-0">
|
||||
<div className="py-1">
|
||||
<Skeleton className="w-16 h-4" />
|
||||
</div>
|
||||
<div className="py-1">
|
||||
<Skeleton className="w-full h-4" />
|
||||
</div>
|
||||
<div className="py-1">
|
||||
<Skeleton className="w-12 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,14 +1,10 @@
|
|||
import { useFetchEvent } from '@/hooks'
|
||||
import { toNote } from '@/lib/link'
|
||||
import { generateBech32IdFromETag, tagNameEquals } from '@/lib/tag'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { Vote } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
import ContentPreview from '../../ContentPreview'
|
||||
import { FormattedTimestamp } from '../../FormattedTimestamp'
|
||||
import UserAvatar from '../../UserAvatar'
|
||||
import Notification from './Notification'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export function PollResponseNotification({
|
||||
notification,
|
||||
|
|
@ -17,7 +13,7 @@ export function PollResponseNotification({
|
|||
notification: Event
|
||||
isNew?: boolean
|
||||
}) {
|
||||
const { push } = useSecondaryPage()
|
||||
const { t } = useTranslation()
|
||||
const eventId = useMemo(() => {
|
||||
const eTag = notification.tags.find(tagNameEquals('e'))
|
||||
return eTag ? generateBech32IdFromETag(eTag) : undefined
|
||||
|
|
@ -29,19 +25,14 @@ export function PollResponseNotification({
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex gap-2 items-center cursor-pointer py-2"
|
||||
onClick={() => push(toNote(pollEvent))}
|
||||
>
|
||||
<UserAvatar userId={notification.pubkey} size="small" />
|
||||
<Vote size={24} className="text-violet-400" />
|
||||
<ContentPreview
|
||||
className={cn('truncate flex-1 w-0', isNew ? 'font-semibold' : 'text-muted-foreground')}
|
||||
event={pollEvent}
|
||||
/>
|
||||
<div className="text-muted-foreground">
|
||||
<FormattedTimestamp timestamp={notification.created_at} short />
|
||||
</div>
|
||||
</div>
|
||||
<Notification
|
||||
notificationId={notification.id}
|
||||
icon={<Vote size={24} className="text-violet-400" />}
|
||||
sender={notification.pubkey}
|
||||
sentAt={notification.created_at}
|
||||
targetEvent={pollEvent}
|
||||
description={t('voted in your poll')}
|
||||
isNew={isNew}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
import Image from '@/components/Image'
|
||||
import { useFetchEvent } from '@/hooks'
|
||||
import { toNote } from '@/lib/link'
|
||||
import { generateBech32IdFromETag, tagNameEquals } from '@/lib/tag'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { Heart } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
import ContentPreview from '../../ContentPreview'
|
||||
import { FormattedTimestamp } from '../../FormattedTimestamp'
|
||||
import UserAvatar from '../../UserAvatar'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Notification from './Notification'
|
||||
|
||||
export function ReactionNotification({
|
||||
notification,
|
||||
|
|
@ -19,7 +15,7 @@ export function ReactionNotification({
|
|||
notification: Event
|
||||
isNew?: boolean
|
||||
}) {
|
||||
const { push } = useSecondaryPage()
|
||||
const { t } = useTranslation()
|
||||
const { pubkey } = useNostr()
|
||||
const eventId = useMemo(() => {
|
||||
const targetPubkey = notification.tags.findLast(tagNameEquals('p'))?.[1]
|
||||
|
|
@ -58,21 +54,14 @@ export function ReactionNotification({
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer py-2"
|
||||
onClick={() => push(toNote(event))}
|
||||
>
|
||||
<div className="flex gap-2 items-center flex-1">
|
||||
<UserAvatar userId={notification.pubkey} size="small" />
|
||||
<div className="text-xl min-w-6 text-center">{reaction}</div>
|
||||
<ContentPreview
|
||||
className={cn('truncate flex-1 w-0', isNew ? 'font-semibold' : 'text-muted-foreground')}
|
||||
event={event}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
<FormattedTimestamp timestamp={notification.created_at} short />
|
||||
</div>
|
||||
</div>
|
||||
<Notification
|
||||
notificationId={notification.id}
|
||||
icon={<div className="text-xl min-w-6 text-center">{reaction}</div>}
|
||||
sender={notification.pubkey}
|
||||
sentAt={notification.created_at}
|
||||
targetEvent={event}
|
||||
description={t('reacted to your note')}
|
||||
isNew={isNew}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
import { toNote } from '@/lib/link'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import client from '@/services/client.service'
|
||||
import { Repeat } from 'lucide-react'
|
||||
import { Event, validateEvent } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
import ContentPreview from '../../ContentPreview'
|
||||
import { FormattedTimestamp } from '../../FormattedTimestamp'
|
||||
import UserAvatar from '../../UserAvatar'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Notification from './Notification'
|
||||
|
||||
export function RepostNotification({
|
||||
notification,
|
||||
|
|
@ -16,7 +12,7 @@ export function RepostNotification({
|
|||
notification: Event
|
||||
isNew?: boolean
|
||||
}) {
|
||||
const { push } = useSecondaryPage()
|
||||
const { t } = useTranslation()
|
||||
const event = useMemo(() => {
|
||||
try {
|
||||
const event = JSON.parse(notification.content) as Event
|
||||
|
|
@ -31,19 +27,14 @@ export function RepostNotification({
|
|||
if (!event) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex gap-2 items-center cursor-pointer py-2"
|
||||
onClick={() => push(toNote(event))}
|
||||
>
|
||||
<UserAvatar userId={notification.pubkey} size="small" />
|
||||
<Repeat size={24} className="text-green-400" />
|
||||
<ContentPreview
|
||||
className={cn('truncate flex-1 w-0', isNew ? 'font-semibold' : 'text-muted-foreground')}
|
||||
event={event}
|
||||
/>
|
||||
<div className="text-muted-foreground">
|
||||
<FormattedTimestamp timestamp={notification.created_at} short />
|
||||
</div>
|
||||
</div>
|
||||
<Notification
|
||||
notificationId={notification.id}
|
||||
icon={<Repeat size={24} className="text-green-400" />}
|
||||
sender={notification.pubkey}
|
||||
sentAt={notification.created_at}
|
||||
targetEvent={event}
|
||||
description={t('reposted your note')}
|
||||
isNew={isNew}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,11 @@
|
|||
import { useFetchEvent } from '@/hooks'
|
||||
import { getZapInfoFromEvent } from '@/lib/event-metadata'
|
||||
import { formatAmount } from '@/lib/lightning'
|
||||
import { toNote, toProfile } from '@/lib/link'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { Zap } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ContentPreview from '../../ContentPreview'
|
||||
import { FormattedTimestamp } from '../../FormattedTimestamp'
|
||||
import UserAvatar from '../../UserAvatar'
|
||||
import Notification from './Notification'
|
||||
|
||||
export function ZapNotification({
|
||||
notification,
|
||||
|
|
@ -21,38 +15,28 @@ export function ZapNotification({
|
|||
isNew?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { push } = useSecondaryPage()
|
||||
const { pubkey } = useNostr()
|
||||
const { senderPubkey, eventId, amount, comment } = useMemo(
|
||||
() => getZapInfoFromEvent(notification) ?? ({} as any),
|
||||
[notification]
|
||||
)
|
||||
const { event, isFetching } = useFetchEvent(eventId)
|
||||
const { event } = useFetchEvent(eventId)
|
||||
|
||||
if (!senderPubkey || !amount) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer py-2"
|
||||
onClick={() => (eventId ? push(toNote(eventId)) : pubkey ? push(toProfile(pubkey)) : null)}
|
||||
>
|
||||
<div className="flex gap-2 items-center flex-1 w-0">
|
||||
<UserAvatar userId={senderPubkey} size="small" />
|
||||
<Zap size={24} className="text-yellow-400 shrink-0" />
|
||||
<Notification
|
||||
notificationId={notification.id}
|
||||
icon={<Zap size={24} className="text-yellow-400 shrink-0" />}
|
||||
sender={senderPubkey}
|
||||
sentAt={notification.created_at}
|
||||
targetEvent={event}
|
||||
middle={
|
||||
<div className="font-semibold text-yellow-400 shrink-0">
|
||||
{formatAmount(amount)} {t('sats')}
|
||||
{formatAmount(amount)} {t('sats')} {comment}
|
||||
</div>
|
||||
{comment && <div className="text-yellow-400 truncate">{comment}</div>}
|
||||
{eventId && !isFetching && (
|
||||
<ContentPreview
|
||||
className={cn('truncate flex-1 w-0', isNew ? 'font-semibold' : 'text-muted-foreground')}
|
||||
event={event}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-muted-foreground shrink-0">
|
||||
<FormattedTimestamp timestamp={notification.created_at} short />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
description={event ? t('zapped your note') : t('zapped you')}
|
||||
isNew={isNew}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue