26 changed files with 520 additions and 234 deletions
@ -0,0 +1,117 @@
@@ -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> |
||||
) |
||||
} |
||||
Loading…
Reference in new issue