import { useState, useRef } from 'react'; import type { Contact, Conversation } from '../types'; import { getContactDisplayName } from '../utils/pubkey'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from './ui/dialog'; import { Tabs, TabsList, TabsTrigger, TabsContent } from './ui/tabs'; import { Input } from './ui/input'; import { Label } from './ui/label'; import { Checkbox } from './ui/checkbox'; import { Button } from './ui/button'; type Tab = 'existing' | 'new-contact' | 'new-room' | 'hashtag'; interface NewMessageModalProps { open: boolean; contacts: Contact[]; undecryptedCount: number; onClose: () => void; onSelectConversation: (conversation: Conversation) => void; onCreateContact: (name: string, publicKey: string, tryHistorical: boolean) => Promise; onCreateChannel: (name: string, key: string, tryHistorical: boolean) => Promise; onCreateHashtagChannel: (name: string, tryHistorical: boolean) => Promise; } export function NewMessageModal({ open, contacts, undecryptedCount, onClose, onSelectConversation, onCreateContact, onCreateChannel, onCreateHashtagChannel, }: NewMessageModalProps) { const [tab, setTab] = useState('existing'); const [name, setName] = useState(''); const [contactKey, setContactKey] = useState(''); const [roomKey, setRoomKey] = useState(''); const [tryHistorical, setTryHistorical] = useState(false); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const hashtagInputRef = useRef(null); const handleCreate = async () => { setError(''); setLoading(true); try { if (tab === 'new-contact') { if (!name.trim() || !contactKey.trim()) { setError('Name and public key are required'); return; } // handleCreateContact sets activeConversation with the backend-normalized key await onCreateContact(name.trim(), contactKey.trim(), tryHistorical); } else if (tab === 'new-room') { if (!name.trim() || !roomKey.trim()) { setError('Room name and key are required'); return; } await onCreateChannel(name.trim(), roomKey.trim(), tryHistorical); } else if (tab === 'hashtag') { const channelName = name.trim(); const validationError = validateHashtagName(channelName); if (validationError) { setError(validationError); return; } await onCreateHashtagChannel(`#${channelName}`, tryHistorical); } onClose(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create'); } finally { setLoading(false); } }; const validateHashtagName = (channelName: string): string | null => { if (!channelName) { return 'Channel name is required'; } if (!/^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$/.test(channelName)) { return 'Use letters, numbers, and single dashes (no leading/trailing dashes)'; } return null; }; const handleCreateAndAddAnother = async () => { setError(''); const channelName = name.trim(); const validationError = validateHashtagName(channelName); if (validationError) { setError(validationError); return; } setLoading(true); try { await onCreateHashtagChannel(`#${channelName}`, tryHistorical); setName(''); hashtagInputRef.current?.focus(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create'); } finally { setLoading(false); } }; const showHistoricalOption = tab !== 'existing' && undecryptedCount > 0; return ( !isOpen && onClose()}> New Conversation {tab === 'existing' && 'Select an existing contact to start a conversation'} {tab === 'new-contact' && 'Add a new contact by entering their name and public key'} {tab === 'new-room' && 'Create a private room with a shared encryption key'} {tab === 'hashtag' && 'Join a public hashtag channel'} setTab(v as Tab)} className="w-full"> Existing Contact Room Hashtag
{contacts.length === 0 ? (
No contacts available
) : ( contacts.map((contact) => (
{ onSelectConversation({ type: 'contact', id: contact.public_key, name: getContactDisplayName(contact.name, contact.public_key), }); onClose(); }} > {getContactDisplayName(contact.name, contact.public_key)}
)) )}
setName(e.target.value)} placeholder="Contact name" />
setContactKey(e.target.value)} placeholder="64-character hex public key" />
setName(e.target.value)} placeholder="Room name" />
setRoomKey(e.target.value)} placeholder="Pre-shared key (hex)" className="flex-1" />
# setName(e.target.value)} placeholder="channel-name" className="flex-1" />
{showHistoricalOption && (
setTryHistorical(checked === true)} />
{tryHistorical && (

Messages will stream in as they decrypt in the background

)}
)} {error &&
{error}
} {tab === 'hashtag' && ( )} {tab !== 'existing' && ( )}
); }