import { useEffect, useRef, useState } from 'react'; import { Dice5 } from 'lucide-react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from './ui/dialog'; import { Tabs, TabsContent, TabsList, TabsTrigger } 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' | 'bulk-hashtag'; interface BulkParseResult { channelNames: string[]; invalidNames: string[]; } interface NewMessageModalProps { open: boolean; undecryptedCount: number; showBulkAddChannelTab?: boolean; prefillRequest?: { tab: 'hashtag'; hashtagName: string; nonce: number; } | null; onClose: () => void; onCreateContact: (name: string, publicKey: string, tryHistorical: boolean) => Promise; onCreateChannel: (name: string, key: string, tryHistorical: boolean) => Promise; onCreateHashtagChannel: (name: string, tryHistorical: boolean) => Promise; onBulkAddHashtagChannels: (channelNames: string[], tryHistorical: boolean) => Promise; } function 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; } function parseBulkHashtagNames(rawText: string, permitCapitals: boolean): BulkParseResult { const tokens = rawText .split(/[\s,]+/) .map((token) => token.trim()) .filter(Boolean); const invalidNames: string[] = []; const channelNames: string[] = []; const seen = new Set(); for (const token of tokens) { const stripped = token.replace(/^#+/, ''); const validationError = validateHashtagName(stripped); if (validationError) { invalidNames.push(token); continue; } const normalized = permitCapitals ? stripped : stripped.toLowerCase(); const channelName = `#${normalized}`; if (seen.has(channelName)) { continue; } seen.add(channelName); channelNames.push(channelName); } return { channelNames, invalidNames }; } export function NewMessageModal({ open, undecryptedCount, showBulkAddChannelTab = false, prefillRequest = null, onClose, onCreateContact, onCreateChannel, onCreateHashtagChannel, onBulkAddHashtagChannels, }: NewMessageModalProps) { const [tab, setTab] = useState('new-contact'); const [name, setName] = useState(''); const [contactKey, setContactKey] = useState(''); const [channelKey, setChannelKey] = useState(''); const [bulkChannelText, setBulkChannelText] = 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 bulkTextareaRef = useRef(null); const resetForm = () => { setName(''); setContactKey(''); setChannelKey(''); setBulkChannelText(''); setTryHistorical(false); setPermitCapitals(false); setError(''); }; useEffect(() => { if (!open) { return; } if (prefillRequest) { setTab(prefillRequest.tab); setName(prefillRequest.hashtagName); setContactKey(''); setChannelKey(''); setBulkChannelText(''); setTryHistorical(false); setPermitCapitals(false); setError(''); setLoading(false); requestAnimationFrame(() => { hashtagInputRef.current?.focus(); }); return; } if (showBulkAddChannelTab) { setTab('bulk-hashtag'); setName(''); setContactKey(''); setChannelKey(''); setBulkChannelText(''); setTryHistorical(false); setPermitCapitals(false); setError(''); setLoading(false); requestAnimationFrame(() => { bulkTextareaRef.current?.focus(); }); return; } setTab('new-contact'); }, [open, prefillRequest, showBulkAddChannelTab]); const handleCreate = async () => { setError(''); setLoading(true); try { if (tab === 'new-contact') { if (!name.trim() || !contactKey.trim()) { setError('Name and public key are required'); return; } 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; } const normalizedName = permitCapitals ? channelName : channelName.toLowerCase(); await onCreateHashtagChannel(`#${normalizedName}`, tryHistorical); } else { const { channelNames, invalidNames } = parseBulkHashtagNames( bulkChannelText, permitCapitals ); if (channelNames.length === 0) { setError('Enter at least one valid room name'); return; } if (invalidNames.length > 0) { setError(`Invalid room names: ${invalidNames.join(', ')}`); return; } await onBulkAddHashtagChannels(channelNames, 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 handleCreateAndAddAnother = async () => { setError(''); const channelName = name.trim(); const validationError = validateHashtagName(channelName); if (validationError) { setError(validationError); return; } setLoading(true); try { 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'} {tab === 'bulk-hashtag' && 'Paste multiple hashtag rooms to add them in one batch'} { setTab(value as Tab); resetForm(); }} className="w-full" > Contact Private Channel Hashtag Channel {showBulkAddChannelTab && ( Bulk Add 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

{showBulkAddChannelTab && (