InboxContent.tsx raw
1 import { toDMConversation } from '@/lib/link'
2 import { useSecondaryPage } from '@/PageManager'
3 import { useDM } from '@/providers/DMProvider'
4 import { useNostr } from '@/providers/NostrProvider'
5 import { nip19 } from 'nostr-tools'
6 import { Loader2, Plus, RefreshCw, X } from 'lucide-react'
7 import { useRef, useState } from 'react'
8 import { useTranslation } from 'react-i18next'
9 import ConversationList from './ConversationList'
10 import { Button } from '../ui/button'
11 import RelayConfigurationRequired from '../RelayConfigurationRequired'
12
13 export default function InboxContent() {
14 const { t } = useTranslation()
15 const { relayList } = useNostr()
16 const { isLoading, error, refreshConversations, startConversation } = useDM()
17 const { push } = useSecondaryPage()
18 const [showNewDM, setShowNewDM] = useState(false)
19 const [newDMInput, setNewDMInput] = useState('')
20 const [newDMError, setNewDMError] = useState('')
21 const inputRef = useRef<HTMLInputElement>(null)
22
23 // Check if user has relay list configured for DMs
24 const hasRelayList = relayList && (relayList.read.length > 0 || relayList.write.length > 0)
25
26 if (!hasRelayList) {
27 return (
28 <div className="p-4">
29 <RelayConfigurationRequired type="dm" />
30 </div>
31 )
32 }
33
34 if (isLoading) {
35 return (
36 <div className="flex items-center justify-center h-64">
37 <div className="flex flex-col items-center gap-2 text-muted-foreground">
38 <Loader2 className="size-8 animate-spin" />
39 <span className="text-sm">{t('Loading messages...')}</span>
40 </div>
41 </div>
42 )
43 }
44
45 if (error) {
46 return (
47 <div className="flex flex-col items-center justify-center h-64 gap-4 text-muted-foreground">
48 <p>{error}</p>
49 <Button onClick={refreshConversations} variant="outline" size="sm" className="gap-2">
50 <RefreshCw className="size-4" />
51 {t('Retry')}
52 </Button>
53 </div>
54 )
55 }
56
57 const handleNewDM = () => {
58 setNewDMError('')
59 const input = newDMInput.trim()
60 if (!input) return
61
62 let hexPubkey: string
63 try {
64 if (input.startsWith('npub1')) {
65 const decoded = nip19.decode(input)
66 if (decoded.type !== 'npub') {
67 setNewDMError(t('Invalid npub'))
68 return
69 }
70 hexPubkey = decoded.data
71 } else if (/^[0-9a-f]{64}$/i.test(input)) {
72 hexPubkey = input.toLowerCase()
73 } else {
74 setNewDMError(t('Enter an npub or 64-char hex pubkey'))
75 return
76 }
77 } catch {
78 setNewDMError(t('Invalid npub'))
79 return
80 }
81
82 startConversation(hexPubkey)
83 push(toDMConversation(hexPubkey))
84 setShowNewDM(false)
85 setNewDMInput('')
86 }
87
88 // Conversations list - clicking opens in secondary panel (or overlay on mobile)
89 return (
90 <div className="h-[calc(100vh-8rem)]">
91 <div className="px-3 py-2 border-b flex items-center gap-2">
92 {showNewDM ? (
93 <div className="flex-1 flex items-center gap-2">
94 <input
95 ref={inputRef}
96 type="text"
97 value={newDMInput}
98 onChange={(e) => { setNewDMInput(e.target.value); setNewDMError('') }}
99 onKeyDown={(e) => e.key === 'Enter' && handleNewDM()}
100 placeholder="npub1..."
101 className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
102 autoFocus
103 />
104 <Button size="sm" variant="ghost" onClick={handleNewDM}>
105 {t('Go')}
106 </Button>
107 <Button size="sm" variant="ghost" onClick={() => { setShowNewDM(false); setNewDMInput(''); setNewDMError('') }}>
108 <X className="size-4" />
109 </Button>
110 </div>
111 ) : (
112 <Button
113 size="sm"
114 variant="ghost"
115 className="gap-1.5 text-muted-foreground hover:text-foreground"
116 onClick={() => setShowNewDM(true)}
117 >
118 <Plus className="size-4" />
119 {t('New DM')}
120 </Button>
121 )}
122 </div>
123 {newDMError && (
124 <div className="px-3 py-1 text-xs text-destructive">{newDMError}</div>
125 )}
126 <ConversationList />
127 </div>
128 )
129 }
130