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