Add DM inbox with NIP-04/NIP-17 support and soft delete
Features: - Full DM inbox UI with conversation list and message view - Support for both NIP-04 (kind 4) and NIP-17 (kind 14/1059) encryption - Progressive message decryption with background loading - Soft delete using kind 30078 Application Specific Data events - Message selection UI with delete selected/delete all - Undelete all functionality per conversation - Jump to newest button with new message counter - Conversation filtering (all / follows only) - Per-conversation relay and encryption settings - New messages indicator on sidebar (clears when inbox viewed) - Follow indicator on conversation items 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
81
src/components/Inbox/ConversationItem.tsx
Normal file
81
src/components/Inbox/ConversationItem.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import UserAvatar from '@/components/UserAvatar'
|
||||
import { formatTimestamp } from '@/lib/timestamp'
|
||||
import { cn } from '@/lib/utils'
|
||||
import client from '@/services/client.service'
|
||||
import { TConversation, TProfile } from '@/types'
|
||||
import { Lock, Users } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface ConversationItemProps {
|
||||
conversation: TConversation
|
||||
isActive: boolean
|
||||
isFollowing: boolean
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export default function ConversationItem({
|
||||
conversation,
|
||||
isActive,
|
||||
isFollowing,
|
||||
onClick
|
||||
}: ConversationItemProps) {
|
||||
const [profile, setProfile] = useState<TProfile | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProfileData = async () => {
|
||||
try {
|
||||
const profileData = await client.fetchProfile(conversation.partnerPubkey)
|
||||
if (profileData) {
|
||||
setProfile(profileData)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch profile:', error)
|
||||
}
|
||||
}
|
||||
fetchProfileData()
|
||||
}, [conversation.partnerPubkey])
|
||||
|
||||
const displayName = profile?.username || conversation.partnerPubkey.slice(0, 8) + '...'
|
||||
const formattedTime = formatTimestamp(conversation.lastMessageAt)
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'w-full flex items-start gap-3 p-3 hover:bg-accent/50 transition-colors text-left',
|
||||
isActive && 'bg-accent'
|
||||
)}
|
||||
>
|
||||
<UserAvatar userId={conversation.partnerPubkey} className="size-10 flex-shrink-0" />
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<span className="font-medium text-sm truncate">{displayName}</span>
|
||||
{isFollowing && (
|
||||
<span className="text-xs text-primary flex-shrink-0" title="Following">
|
||||
<Users className="size-3" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground flex-shrink-0">{formattedTime}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 mt-0.5">
|
||||
{conversation.preferredEncryption === 'nip17' && (
|
||||
<span title="NIP-17 encrypted">
|
||||
<Lock className="size-3 text-green-500 flex-shrink-0" />
|
||||
</span>
|
||||
)}
|
||||
<p className="text-sm text-muted-foreground truncate">{conversation.lastMessagePreview}</p>
|
||||
</div>
|
||||
|
||||
{conversation.unreadCount > 0 && (
|
||||
<span className="inline-flex items-center justify-center size-5 text-xs rounded-full bg-primary text-primary-foreground mt-1">
|
||||
{conversation.unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user