diff --git a/src/app/api/swarm/chat/conversations/[id]/route.ts b/src/app/api/swarm/chat/conversations/[id]/route.ts new file mode 100644 index 0000000..36ca0cf --- /dev/null +++ b/src/app/api/swarm/chat/conversations/[id]/route.ts @@ -0,0 +1,66 @@ +/** + * Swarm Chat Conversation Management + * + * DELETE: Delete a conversation (for self or both parties) + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { db, chatConversations, chatMessages } from '@/db'; +import { eq, and } from 'drizzle-orm'; +import { getSession } from '@/lib/auth'; + +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + if (!db) { + return NextResponse.json({ error: 'Database not available' }, { status: 503 }); + } + + const session = await getSession(); + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const deleteFor = searchParams.get('deleteFor'); // 'self' or 'both' + + // Verify the conversation belongs to this user + const conversation = await db.query.chatConversations.findFirst({ + where: and( + eq(chatConversations.id, params.id), + eq(chatConversations.participant1Id, session.user.id) + ), + }); + + if (!conversation) { + return NextResponse.json({ error: 'Conversation not found' }, { status: 404 }); + } + + if (deleteFor === 'both') { + // Delete the entire conversation and all messages (cascade will handle messages) + await db.delete(chatConversations).where(eq(chatConversations.id, params.id)); + + // TODO: Send a federation message to the other party to delete their copy + // This would require implementing a swarm protocol for conversation deletion + + return NextResponse.json({ + success: true, + message: 'Conversation deleted for both parties' + }); + } else { + // Delete for self only - just delete the conversation record + // The other party will still have their copy + await db.delete(chatConversations).where(eq(chatConversations.id, params.id)); + + return NextResponse.json({ + success: true, + message: 'Conversation deleted for you' + }); + } + } catch (error) { + console.error('Delete conversation error:', error); + return NextResponse.json({ error: 'Failed to delete conversation' }, { status: 500 }); + } +} diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index f52c29f..91d85c0 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useRef } from 'react'; import { useAuth } from '@/lib/contexts/AuthContext'; import { useChatEncryption } from '@/lib/hooks/useChatEncryption'; -import { ArrowLeft, Send, Lock, Shield, Loader2, MessageCircle, Search, Plus } from 'lucide-react'; +import { ArrowLeft, Send, Lock, Shield, Loader2, MessageCircle, Search, Plus, Trash2, MoreVertical } from 'lucide-react'; import { formatFullHandle } from '@/lib/utils/handle'; import { useRouter } from 'next/navigation'; @@ -50,6 +50,9 @@ export default function ChatPage() { const [sending, setSending] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [recipientPublicKey, setRecipientPublicKey] = useState(null); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [conversationToDelete, setConversationToDelete] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); // Password/Key State const [showPasswordInput, setShowPasswordInput] = useState(false); @@ -324,6 +327,36 @@ export default function ChatPage() { } }; + const handleDeleteConversation = async (deleteFor: 'self' | 'both') => { + if (!conversationToDelete) return; + setIsDeleting(true); + try { + const res = await fetch(`/api/swarm/chat/conversations/${conversationToDelete.id}?deleteFor=${deleteFor}`, { + method: 'DELETE', + }); + + if (res.ok) { + // Remove from local state + setConversations(prev => prev.filter(c => c.id !== conversationToDelete.id)); + + // If we're viewing this conversation, go back to list + if (selectedConversation?.id === conversationToDelete.id) { + setSelectedConversation(null); + } + + setShowDeleteModal(false); + setConversationToDelete(null); + } else { + alert('Failed to delete conversation'); + } + } catch (err) { + console.error('Delete error:', err); + alert('Failed to delete conversation'); + } finally { + setIsDeleting(false); + } + }; + const filteredConversations = conversations.filter((conv) => conv.participant2.displayName?.toLowerCase().includes(searchQuery.toLowerCase()) || conv.participant2.handle.toLowerCase().includes(searchQuery.toLowerCase()) @@ -497,6 +530,35 @@ export default function ChatPage() { {formatFullHandle(selectedConversation.participant2.handle)} + @@ -713,11 +775,19 @@ export default function ChatPage() { filteredConversations.map(conv => (
setSelectedConversation(conv)} className="post" - style={{ cursor: 'pointer' }} + style={{ + cursor: 'pointer', + display: 'flex', + alignItems: 'flex-start', + gap: '12px', + position: 'relative' + }} > -
+
setSelectedConversation(conv)} + style={{ display: 'flex', alignItems: 'flex-start', gap: '12px', flex: 1, minWidth: 0 }} + >
{conv.participant2.avatarUrl ? ( @@ -758,9 +828,147 @@ export default function ChatPage() {
+
)) )} + + {/* Delete Confirmation Modal */} + {showDeleteModal && conversationToDelete && ( +
{ + if (!isDeleting) { + setShowDeleteModal(false); + setConversationToDelete(null); + } + }} + > +
e.stopPropagation()} + > +
+
+ +
+

+ Delete Conversation +

+
+ +

+ Delete your conversation with {conversationToDelete.participant2.displayName}? +

+ +
+ + + + + +
+
+
+ )} ); }