Files
smesh/src/components/Inbox/MessageInfoModal.tsx
woikos fecd4fdd45 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>
2025-12-31 11:06:51 +01:00

134 lines
3.8 KiB
TypeScript

import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import dmService from '@/services/dm.service'
import { TDirectMessage } from '@/types'
import { Loader2, RefreshCw, Server } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
interface MessageInfoModalProps {
message: TDirectMessage | null
open: boolean
onOpenChange: (open: boolean) => void
onRelaysUpdated?: (relays: string[]) => void
}
export default function MessageInfoModal({
message,
open,
onOpenChange,
onRelaysUpdated
}: MessageInfoModalProps) {
const { t } = useTranslation()
const [isChecking, setIsChecking] = useState(false)
const [additionalRelays, setAdditionalRelays] = useState<string[]>([])
if (!message) return null
const allRelays = [...(message.seenOnRelays || []), ...additionalRelays]
const uniqueRelays = [...new Set(allRelays)]
const handleCheckOtherRelays = async () => {
setIsChecking(true)
try {
const foundRelays = await dmService.checkOtherRelaysForEvent(
message.id,
uniqueRelays
)
if (foundRelays.length > 0) {
const newRelays = [...additionalRelays, ...foundRelays]
setAdditionalRelays(newRelays)
onRelaysUpdated?.([...(message.seenOnRelays || []), ...newRelays])
}
} catch (error) {
console.error('Failed to check other relays:', error)
} finally {
setIsChecking(false)
}
}
const formatRelayUrl = (url: string) => {
try {
const parsed = new URL(url)
return parsed.hostname + (parsed.pathname !== '/' ? parsed.pathname : '')
} catch {
return url
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Server className="size-4" />
{t('Message Info')}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{/* Protocol */}
<div>
<span className="text-sm font-medium text-muted-foreground">
{t('Encryption')}
</span>
<p className="text-sm mt-1">
{message.encryptionType === 'nip17' ? 'NIP-44 (Gift Wrap)' : 'NIP-04 (Legacy)'}
</p>
</div>
{/* Relays */}
<div>
<span className="text-sm font-medium text-muted-foreground">
{t('Seen on relays')}
</span>
{uniqueRelays.length > 0 ? (
<ul className="mt-1 space-y-1">
{uniqueRelays.map((relay) => (
<li
key={relay}
className="text-sm font-mono bg-muted px-2 py-1 rounded truncate"
title={relay}
>
{formatRelayUrl(relay)}
</li>
))}
</ul>
) : (
<p className="text-sm text-muted-foreground mt-1">
{t('No relay information available')}
</p>
)}
</div>
{/* Check other relays button */}
<Button
variant="outline"
size="sm"
className="w-full"
onClick={handleCheckOtherRelays}
disabled={isChecking}
>
{isChecking ? (
<>
<Loader2 className="size-4 mr-2 animate-spin" />
{t('Checking...')}
</>
) : (
<>
<RefreshCw className="size-4 mr-2" />
{t('Check for other relays')}
</>
)}
</Button>
</div>
</DialogContent>
</Dialog>
)
}