ConversationItem.tsx raw

   1  import UserAvatar from '@/components/UserAvatar'
   2  import { useKeyboardNavigable } from '@/hooks/useKeyboardNavigable'
   3  import { formatTimestamp } from '@/lib/timestamp'
   4  import { cn } from '@/lib/utils'
   5  import client from '@/services/client.service'
   6  import { TConversation, TProfile } from '@/types'
   7  import { Lock, Users, X } from 'lucide-react'
   8  import { useCallback, useEffect, useRef, useState } from 'react'
   9  
  10  interface ConversationItemProps {
  11    conversation: TConversation
  12    isActive: boolean
  13    isFollowing: boolean
  14    onClick: () => void
  15    onClose?: () => void
  16    navIndex?: number
  17  }
  18  
  19  export default function ConversationItem({
  20    conversation,
  21    isActive,
  22    isFollowing,
  23    onClick,
  24    onClose,
  25    navIndex
  26  }: ConversationItemProps) {
  27    const [profile, setProfile] = useState<TProfile | null>(null)
  28    const buttonRef = useRef<HTMLButtonElement>(null)
  29  
  30    const handleActivate = useCallback(() => {
  31      buttonRef.current?.click()
  32    }, [])
  33  
  34    const { ref: navRef, isSelected } = useKeyboardNavigable(1, navIndex ?? 0, {
  35      meta: { type: 'sidebar', onActivate: handleActivate }
  36    })
  37  
  38    useEffect(() => {
  39      const fetchProfileData = async () => {
  40        try {
  41          const profileData = await client.fetchProfile(conversation.partnerPubkey)
  42          if (profileData) {
  43            setProfile(profileData)
  44          }
  45        } catch (error) {
  46          console.error('Failed to fetch profile:', error)
  47        }
  48      }
  49      fetchProfileData()
  50    }, [conversation.partnerPubkey])
  51  
  52    const displayName = profile?.username || conversation.partnerPubkey.slice(0, 8) + '...'
  53    const formattedTime = formatTimestamp(conversation.lastMessageAt)
  54  
  55    return (
  56      <div ref={navRef} className="scroll-mt-[6.5rem]">
  57        <button
  58          ref={buttonRef}
  59          onClick={onClick}
  60          className={cn(
  61            'w-full flex items-start gap-3 p-3 hover:bg-accent/50 transition-colors text-left',
  62            isActive && 'bg-accent',
  63            isSelected && 'ring-2 ring-primary ring-inset'
  64          )}
  65        >
  66        <UserAvatar userId={conversation.partnerPubkey} className="size-10 flex-shrink-0" />
  67  
  68        <div className="flex-1 min-w-0">
  69          <div className="flex items-center justify-between gap-2">
  70            <div className="flex items-center gap-1.5 min-w-0">
  71              <span className="font-medium text-sm truncate">{displayName}</span>
  72              {isFollowing && (
  73                <span className="text-xs text-primary flex-shrink-0" title="Following">
  74                  <Users className="size-3" />
  75                </span>
  76              )}
  77            </div>
  78            <div className="flex items-center gap-1 flex-shrink-0">
  79              <span className="text-xs text-muted-foreground">{formattedTime}</span>
  80              {isActive && onClose && (
  81                <button
  82                  onClick={(e) => {
  83                    e.stopPropagation()
  84                    onClose()
  85                  }}
  86                  className="p-0.5 rounded hover:bg-muted-foreground/20 transition-colors"
  87                  title="Close conversation"
  88                >
  89                  <X className="size-4 text-muted-foreground" />
  90                </button>
  91              )}
  92            </div>
  93          </div>
  94  
  95          <div className="flex items-center gap-1.5 mt-0.5">
  96            {conversation.preferredEncryption === 'nip17' && (
  97              <span title="NIP-17 encrypted">
  98                <Lock className="size-3 text-green-500 flex-shrink-0" />
  99              </span>
 100            )}
 101            <p className="text-sm text-muted-foreground truncate">{conversation.lastMessagePreview}</p>
 102          </div>
 103  
 104          {conversation.unreadCount > 0 && (
 105            <span className="inline-flex items-center justify-center size-5 text-xs rounded-full bg-primary text-primary-foreground mt-1">
 106              {conversation.unreadCount}
 107            </span>
 108          )}
 109        </div>
 110        </button>
 111      </div>
 112    )
 113  }
 114