diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index 023b5e2..4abd514 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -52,12 +52,12 @@ export default function ChatPage() { useEffect(() => { if (selectedConversation && hasKeys) { loadMessages(selectedConversation.id); markAsRead(selectedConversation.id); fetchRecipientKey(selectedConversation.participant2.handle); } }, [selectedConversation, hasKeys]); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); - const fetchRecipientKey = async (handle: string) => { try { const res = await fetch(`/api/users/\${encodeURIComponent(handle)}`); const data = await res.json(); setRecipientPublicKey(data.user?.chatPublicKey || null); } catch { setRecipientPublicKey(null); } }; - const loadConversations = async () => { try { const res = await fetch('/api/swarm/chat/conversations'); const data = await res.json(); setConversations(data.conversations || []); } catch { /* ignore */ } finally { setLoading(false); } }; - const loadMessages = async (conversationId: string) => { try { const res = await fetch(`/api/swarm/chat/messages?conversationId=\${conversationId}`); const data = await res.json(); const decrypted = await Promise.all((data.messages || []).map(async (msg: Message) => { try { const key = msg.isSentByMe ? keys?.publicKey : msg.senderPublicKey || recipientPublicKey; if (key && msg.encryptedContent) return { ...msg, decryptedContent: await decryptMessage(msg.encryptedContent, key) }; } catch { /* ignore */ } return { ...msg, decryptedContent: '[Unable to decrypt]' }; })); setMessages(decrypted); } catch { /* ignore */ } }; - const markAsRead = async (conversationId: string) => { try { await fetch('/api/swarm/chat/messages', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conversationId }) }); setConversations(prev => prev.map(c => c.id === conversationId ? { ...c, unreadCount: 0 } : c)); } catch { /* ignore */ } }; - const sendMessage = async (e: React.FormEvent) => { e.preventDefault(); if (!newMessage.trim() || !selectedConversation || !recipientPublicKey) return; setSending(true); try { const encrypted = await encryptMessage(newMessage, recipientPublicKey); const res = await fetch('/api/swarm/chat/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ recipientHandle: selectedConversation.participant2.handle, encryptedContent: encrypted, senderPublicKey: keys?.publicKey }) }); if (res.ok) { setMessages(prev => [...prev, { id: crypto.randomUUID(), senderHandle: user?.handle || '', encryptedContent: encrypted, decryptedContent: newMessage, isSentByMe: true, createdAt: new Date().toISOString() }]); setNewMessage(''); loadConversations(); } } catch { /* ignore */ } finally { setSending(false); } }; - const startNewChat = async (e: React.FormEvent) => { e.preventDefault(); if (!newChatHandle.trim()) return; setSending(true); try { const cleanHandle = newChatHandle.replace(/^@/, ''); const res = await fetch(`/api/users/\${encodeURIComponent(cleanHandle)}`); const data = await res.json(); if (!data.user?.chatPublicKey) { alert('This user has not enabled encrypted chat yet.'); return; } const encrypted = await encryptMessage('Hey!', data.user.chatPublicKey); const sendRes = await fetch('/api/swarm/chat/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ recipientHandle: cleanHandle, encryptedContent: encrypted, senderPublicKey: keys?.publicKey }) }); if (sendRes.ok) { setShowNewChat(false); setNewChatHandle(''); loadConversations(); } } catch { /* ignore */ } finally { setSending(false); } }; + const fetchRecipientKey = async (handle: string) => { try { const res = await fetch(`/api/users/${encodeURIComponent(handle)}`); const data = await res.json(); setRecipientPublicKey(data.user?.chatPublicKey || null); } catch { setRecipientPublicKey(null); } }; + const loadConversations = async () => { try { const res = await fetch('/api/swarm/chat/conversations'); const data = await res.json(); setConversations(data.conversations || []); } catch { } finally { setLoading(false); } }; + const loadMessages = async (conversationId: string) => { try { const res = await fetch(`/api/swarm/chat/messages?conversationId=${conversationId}`); const data = await res.json(); const decrypted = await Promise.all((data.messages || []).map(async (msg: Message) => { try { const key = msg.isSentByMe ? keys?.publicKey : msg.senderPublicKey || recipientPublicKey; if (key && msg.encryptedContent) return { ...msg, decryptedContent: await decryptMessage(msg.encryptedContent, key) }; } catch { } return { ...msg, decryptedContent: '[Unable to decrypt]' }; })); setMessages(decrypted); } catch { } }; + const markAsRead = async (conversationId: string) => { try { await fetch('/api/swarm/chat/messages', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conversationId }) }); setConversations(prev => prev.map(c => c.id === conversationId ? { ...c, unreadCount: 0 } : c)); } catch { } }; + const sendMessage = async (e: React.FormEvent) => { e.preventDefault(); if (!newMessage.trim() || !selectedConversation || !recipientPublicKey) return; setSending(true); try { const encrypted = await encryptMessage(newMessage, recipientPublicKey); const res = await fetch('/api/swarm/chat/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ recipientHandle: selectedConversation.participant2.handle, encryptedContent: encrypted, senderPublicKey: keys?.publicKey }) }); if (res.ok) { setMessages(prev => [...prev, { id: crypto.randomUUID(), senderHandle: user?.handle || '', encryptedContent: encrypted, decryptedContent: newMessage, isSentByMe: true, createdAt: new Date().toISOString() }]); setNewMessage(''); loadConversations(); } } catch { } finally { setSending(false); } }; + const startNewChat = async (e: React.FormEvent) => { e.preventDefault(); if (!newChatHandle.trim()) return; setSending(true); try { const cleanHandle = newChatHandle.replace(/^@/, ''); const res = await fetch(`/api/users/${encodeURIComponent(cleanHandle)}`); const data = await res.json(); if (!data.user?.chatPublicKey) { alert('This user has not enabled encrypted chat yet.'); return; } const encrypted = await encryptMessage('Hey! 👋', data.user.chatPublicKey); const sendRes = await fetch('/api/swarm/chat/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ recipientHandle: cleanHandle, encryptedContent: encrypted, senderPublicKey: keys?.publicKey }) }); if (sendRes.ok) { setShowNewChat(false); setNewChatHandle(''); loadConversations(); } } catch { } finally { setSending(false); } }; const handlePasswordSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!password) return; setPasswordError(''); setIsProcessingPassword(true); try { if (needsPasswordToRestore) { const success = await restoreKeysWithPassword(password); if (!success) { setPasswordError('Incorrect password.'); return; } } else { await generateAndRegisterKeys(password); } setShowPasswordModal(false); setPassword(''); } catch { setPasswordError('Failed. Please try again.'); } finally { setIsProcessingPassword(false); } }; const filteredConversations = conversations.filter(conv => conv.participant2.displayName?.toLowerCase().includes(searchQuery.toLowerCase()) || conv.participant2.handle.toLowerCase().includes(searchQuery.toLowerCase())); @@ -94,15 +94,15 @@ export default function ChatPage() { return (
-
+

Messages

setSearchQuery(e.target.value)} />
- {loading ?

Loading...

: filteredConversations.length === 0 ?

No conversations

:
{filteredConversations.map((conv) => )}
} + {loading ?

Loading...

: filteredConversations.length === 0 ?

No conversations

:
{filteredConversations.map((conv) => )}
}
-
+
{selectedConversation ? <>
{selectedConversation.participant2.avatarUrl ? : {(selectedConversation.participant2.displayName || selectedConversation.participant2.handle).charAt(0).toUpperCase()}}

{selectedConversation.participant2.displayName}

{formatFullHandle(selectedConversation.participant2.handle)}

-
Messages are end-to-end encrypted.
{messages.map((msg) =>
{!msg.isSentByMe &&
{msg.senderAvatarUrl ? : {(msg.senderDisplayName || msg.senderHandle).charAt(0).toUpperCase()}}
}
{msg.decryptedContent || msg.encryptedContent}
{new Date(msg.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}{msg.isSentByMe && {msg.readAt ? '✓✓' : msg.deliveredAt ? '✓' : '○'}}
)}
+
Messages are end-to-end encrypted.
{messages.map((msg) =>
{!msg.isSentByMe &&
{msg.senderAvatarUrl ? : {(msg.senderDisplayName || msg.senderHandle).charAt(0).toUpperCase()}}
}
{msg.decryptedContent || msg.encryptedContent}
{new Date(msg.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}{msg.isSentByMe && {msg.readAt ? '✓✓' : msg.deliveredAt ? '✓' : '○'}}
)}
setNewMessage(e.target.value)} disabled={sending || !recipientPublicKey} />
:

Select a conversation

}