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