Notification.tsx raw
1 import ContentPreview from '@/components/ContentPreview'
2 import { FormattedTimestamp } from '@/components/FormattedTimestamp'
3 import StuffStats from '@/components/StuffStats'
4 import { Skeleton } from '@/components/ui/skeleton'
5 import UserAvatar from '@/components/UserAvatar'
6 import Username from '@/components/Username'
7 import { NOTIFICATION_LIST_STYLE } from '@/constants'
8 import { useKeyboardNavigable } from '@/hooks/useKeyboardNavigable'
9 import { toNote, toProfile } from '@/lib/link'
10 import { cn } from '@/lib/utils'
11 import { useSecondaryPage } from '@/PageManager'
12 import { useNostr } from '@/providers/NostrProvider'
13 import { useNotification } from '@/providers/NotificationProvider'
14 import { useUserPreferences } from '@/providers/UserPreferencesProvider'
15 import { NostrEvent } from 'nostr-tools'
16 import { useMemo } from 'react'
17 import { useTranslation } from 'react-i18next'
18
19 export default function Notification({
20 icon,
21 notificationId,
22 sender,
23 sentAt,
24 description,
25 middle = null,
26 targetEvent,
27 isNew = false,
28 showStats = false,
29 navIndex
30 }: {
31 icon: React.ReactNode
32 notificationId: string
33 sender: string
34 sentAt: number
35 description: string
36 middle?: React.ReactNode
37 targetEvent?: NostrEvent
38 isNew?: boolean
39 showStats?: boolean
40 navIndex?: number
41 }) {
42 const { t } = useTranslation()
43 const { push } = useSecondaryPage()
44 const { pubkey } = useNostr()
45 const { isNotificationRead, markNotificationAsRead } = useNotification()
46 const { notificationListStyle } = useUserPreferences()
47 const unread = useMemo(
48 () => isNew && !isNotificationRead(notificationId),
49 [isNew, isNotificationRead, notificationId]
50 )
51
52 const { ref: navRef, isSelected } = useKeyboardNavigable(1, navIndex ?? 0, {
53 meta: { type: 'note' }
54 })
55
56 const handleClick = () => {
57 markNotificationAsRead(notificationId)
58 if (targetEvent) {
59 push(toNote(targetEvent.id))
60 } else if (pubkey) {
61 push(toProfile(pubkey))
62 }
63 }
64
65 if (notificationListStyle === NOTIFICATION_LIST_STYLE.COMPACT) {
66 return (
67 <div
68 ref={navRef}
69 className={cn(
70 'flex items-center justify-between cursor-pointer py-2 px-4 scroll-mt-[6.5rem]',
71 isSelected && 'ring-2 ring-primary ring-inset'
72 )}
73 onClick={handleClick}
74 >
75 <div className="flex gap-2 items-center flex-1 w-0">
76 <UserAvatar userId={sender} size="small" />
77 {icon}
78 {middle}
79 {targetEvent && (
80 <ContentPreview
81 className={cn(
82 'truncate flex-1 w-0',
83 unread ? 'font-semibold' : 'text-muted-foreground'
84 )}
85 event={targetEvent}
86 />
87 )}
88 </div>
89 <div className="text-muted-foreground shrink-0">
90 <FormattedTimestamp timestamp={sentAt} short />
91 </div>
92 </div>
93 )
94 }
95
96 return (
97 <div
98 ref={navRef}
99 className={cn(
100 'clickable flex items-start gap-2 cursor-pointer py-2 px-4 border-b scroll-mt-[6.5rem]',
101 isSelected && 'ring-2 ring-primary ring-inset'
102 )}
103 onClick={handleClick}
104 >
105 <div className="flex gap-2 items-center mt-1.5">
106 {icon}
107 <UserAvatar userId={sender} size="medium" />
108 </div>
109 <div className="flex-1 w-0">
110 <div className="flex items-center justify-between gap-1">
111 <div className="flex gap-1 items-center">
112 <Username
113 userId={sender}
114 className="flex-1 max-w-fit truncate font-semibold"
115 skeletonClassName="h-4"
116 />
117 <div className="shrink-0 text-muted-foreground text-sm">{description}</div>
118 </div>
119 {unread && (
120 <button
121 className="m-0.5 size-3 bg-primary rounded-full shrink-0 transition-all hover:ring-4 hover:ring-primary/20"
122 title={t('Mark as read')}
123 onClick={(e) => {
124 e.stopPropagation()
125 markNotificationAsRead(notificationId)
126 }}
127 />
128 )}
129 </div>
130 {middle}
131 {targetEvent && (
132 <ContentPreview
133 className={cn('line-clamp-2', !unread && 'text-muted-foreground')}
134 event={targetEvent}
135 />
136 )}
137 <FormattedTimestamp timestamp={sentAt} className="shrink-0 text-muted-foreground text-sm" />
138 {showStats && targetEvent && <StuffStats stuff={targetEvent} className="mt-1" />}
139 </div>
140 </div>
141 )
142 }
143
144 export function NotificationSkeleton() {
145 const { notificationListStyle } = useUserPreferences()
146
147 if (notificationListStyle === NOTIFICATION_LIST_STYLE.COMPACT) {
148 return (
149 <div className="flex gap-2 items-center h-11 py-2 px-4">
150 <Skeleton className="w-7 h-7 rounded-full" />
151 <Skeleton className="h-6 flex-1 w-0" />
152 </div>
153 )
154 }
155
156 return (
157 <div className="flex items-start gap-2 cursor-pointer py-2 px-4">
158 <div className="flex gap-2 items-center mt-1.5">
159 <Skeleton className="w-6 h-6" />
160 <Skeleton className="w-9 h-9 rounded-full" />
161 </div>
162 <div className="flex-1 w-0">
163 <div className="py-1">
164 <Skeleton className="w-16 h-4" />
165 </div>
166 <div className="py-1">
167 <Skeleton className="w-full h-4" />
168 </div>
169 <div className="py-1">
170 <Skeleton className="w-12 h-4" />
171 </div>
172 </div>
173 </div>
174 )
175 }
176