import { useState, useRef } from 'react'; import { Dice5 } from 'lucide-react'; 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'; import { toast } from './ui/sonner'; type Tab = 'new-contact' | 'new-channel' | 'hashtag'; interface NewMessageModalProps { open: boolean; undecryptedCount: number; onClose: () => 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, undecryptedCount, onClose, onCreateContact, onCreateChannel, onCreateHashtagChannel, }: NewMessageModalProps) { const [tab, setTab] = useState('new-contact'); const [name, setName] = useState(''); const [contactKey, setContactKey] = useState(''); const [channelKey, setChannelKey] = useState(''); const [tryHistorical, setTryHistorical] = useState(false); const [permitCapitals, setPermitCapitals] = useState(false); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const hashtagInputRef = useRef(null); const resetForm = () => { setName(''); setContactKey(''); setChannelKey(''); setTryHistorical(false); setPermitCapitals(false); setError(''); }; 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-channel') { if (!name.trim() || !channelKey.trim()) { setError('Channel name and key are required'); return; } await onCreateChannel(name.trim(), channelKey.trim(), tryHistorical); } else if (tab === 'hashtag') { const channelName = name.trim(); const validationError = validateHashtagName(channelName); if (validationError) { setError(validationError); return; } // Normalize to lowercase unless user explicitly permits capitals const normalizedName = permitCapitals ? channelName : channelName.toLowerCase(); await onCreateHashtagChannel(`#${normalizedName}`, tryHistorical); } resetForm(); onClose(); } catch (err) { toast.error('Failed to create conversation', { description: err instanceof Error ? err.message : undefined, }); 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 { // Normalize to lowercase unless user explicitly permits capitals const normalizedName = permitCapitals ? channelName : channelName.toLowerCase(); await onCreateHashtagChannel(`#${normalizedName}`, tryHistorical); setName(''); hashtagInputRef.current?.focus(); } catch (err) { toast.error('Failed to create conversation', { description: err instanceof Error ? err.message : undefined, }); setError(err instanceof Error ? err.message : 'Failed to create'); } finally { setLoading(false); } }; const showHistoricalOption = undecryptedCount > 0; return ( { if (!isOpen) { resetForm(); onClose(); } }} > New Conversation {tab === 'new-contact' && 'Add a new contact by entering their name and public key'} {tab === 'new-channel' && 'Create a private channel with a shared encryption key'} {tab === 'hashtag' && 'Join a public hashtag channel'} { setTab(v as Tab); resetForm(); }} className="w-full" > Contact Private Channel Hashtag Channel
setName(e.target.value)} placeholder="Contact name" />
setContactKey(e.target.value)} placeholder="64-character hex public key" />
setName(e.target.value)} placeholder="Channel name" />
setChannelKey(e.target.value)} placeholder="Pre-shared key (hex)" className="flex-1" />
# setName(e.target.value)} placeholder="channel-name" className="flex-1" />

Not recommended; most companions normalize to lowercase

{showHistoricalOption && (
setTryHistorical(checked === true)} />
{tryHistorical && (

Messages will stream in as they decrypt in the background

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