diff --git a/src/app/api/chat/send/route.ts b/src/app/api/chat/send/route.ts index c23f82a..b8c7e51 100644 --- a/src/app/api/chat/send/route.ts +++ b/src/app/api/chat/send/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/db'; -import { chatInbox, chatConversations, users } from '@/db/schema'; +import { chatInbox, chatConversations, chatMessages, users } from '@/db/schema'; import { requireSignedAction } from '@/lib/auth/verify-signature'; import { eq, and } from 'drizzle-orm'; import { signedFetch } from '@/lib/api/signed-fetch'; @@ -102,7 +102,47 @@ export async function POST(request: NextRequest) { const remoteUrl = `https://${targetNodeDomain}/api/swarm/chat/inbox`; try { - console.log('[Chat Send] Forwarding to remote:', remoteUrl, 'Envelope:', JSON.stringify(body, null, 2)); + console.log('[Chat Send] Forwarding to remote:', remoteUrl); + + // Get or create conversation for sender + let conversation = await db.query.chatConversations.findFirst({ + where: and( + eq(chatConversations.participant1Id, user.id), + eq(chatConversations.participant2Handle, recipientHandle || recipientDid) + ) + }); + + if (!conversation) { + const [newConv] = await db.insert(chatConversations).values({ + participant1Id: user.id, + participant2Handle: recipientHandle || recipientDid, + lastMessageAt: new Date(), + lastMessagePreview: '[Encrypted Message]' + }).returning(); + conversation = newConv; + console.log('[Chat Send] Created conversation:', conversation.id); + } + + // Store message locally so sender can see it + const messageId = crypto.randomUUID(); + const localDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost'; + const swarmMessageId = `swarm:${localDomain}:${messageId}`; + + const [newMessage] = await db.insert(chatMessages).values({ + conversationId: conversation.id, + senderHandle: user.handle, + senderDisplayName: user.displayName, + senderAvatarUrl: user.avatarUrl, + senderNodeDomain: null, // Local sender + encryptedContent: ciphertext, + senderChatPublicKey: null, // V2 E2E - keys are in the envelope + swarmMessageId, + deliveredAt: null, // Will update when remote confirms + readAt: null, + }).returning(); + + console.log('[Chat Send] Stored local message:', newMessage.id); + const response = await fetch(remoteUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -140,26 +180,14 @@ export async function POST(request: NextRequest) { }, { status: response.status }); } - // Create local conversation for sender if we have recipient handle - if (recipientHandle) { - const existingConv = await db.query.chatConversations.findFirst({ - where: and( - eq(chatConversations.participant1Id, user.id), - eq(chatConversations.participant2Handle, recipientHandle) - ) - }); - - if (!existingConv) { - await db.insert(chatConversations).values({ - participant1Id: user.id, - participant2Handle: recipientHandle, - lastMessageAt: new Date(), - lastMessagePreview: '[Encrypted Message]' - }); - } - } + // Mark message as delivered since remote accepted it + await db.update(chatMessages) + .set({ deliveredAt: new Date() }) + .where(eq(chatMessages.id, newMessage.id)); - return NextResponse.json({ success: true, remote: true }); + console.log('[Chat Send] Message marked as delivered'); + + return NextResponse.json({ success: true, remote: true, messageId: newMessage.id }); } catch (error: any) { console.error('[Chat Send] Remote delivery error:', error); diff --git a/src/app/api/swarm/chat/inbox/route.ts b/src/app/api/swarm/chat/inbox/route.ts index b260476..5f3da13 100644 --- a/src/app/api/swarm/chat/inbox/route.ts +++ b/src/app/api/swarm/chat/inbox/route.ts @@ -50,8 +50,8 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Recipient not found' }, { status: 404 }); } - // Import chatInbox schema - const { chatInbox } = await import('@/db/schema'); + // Import schemas + const { chatInbox, chatConversations, chatMessages } = await import('@/db/schema'); // Store in V2 inbox await db.insert(chatInbox).values({ @@ -62,7 +62,54 @@ export async function POST(request: NextRequest) { expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) }); - console.log(`[Swarm Chat V2] Received encrypted message for ${recipientDid}`); + // Also create conversation and message for recipient UI + // Extract sender info from envelope + const senderHandle = body.handle || body.did; // Fallback to DID if no handle + const senderFullHandle = `${senderHandle}@${body.did?.split(':')[2] || 'unknown'}`; // Extract domain from DID + + // Get or create conversation for recipient + let conversation = await db.query.chatConversations.findFirst({ + where: and( + eq(chatConversations.participant1Id, recipient.id), + eq(chatConversations.participant2Handle, senderFullHandle) + ) + }); + + if (!conversation) { + const [newConv] = await db.insert(chatConversations).values({ + participant1Id: recipient.id, + participant2Handle: senderFullHandle, + lastMessageAt: new Date(), + lastMessagePreview: '[Encrypted message]' + }).returning(); + conversation = newConv; + console.log(`[Swarm Chat V2] Created conversation for recipient:`, conversation.id); + } else { + // Update last message time + await db.update(chatConversations) + .set({ + lastMessageAt: new Date(), + lastMessagePreview: '[Encrypted message]' + }) + .where(eq(chatConversations.id, conversation.id)); + } + + // Store message reference so it appears in UI + const messageId = crypto.randomUUID(); + await db.insert(chatMessages).values({ + conversationId: conversation.id, + senderHandle: senderFullHandle, + senderDisplayName: null, // Unknown until decrypted + senderAvatarUrl: null, + senderNodeDomain: body.did?.split(':')[2] || null, + encryptedContent: ciphertext, + senderChatPublicKey: null, + swarmMessageId: `swarm:v2:${messageId}`, + deliveredAt: new Date(), + readAt: null, + }); + + console.log(`[Swarm Chat V2] Received encrypted message for ${recipientDid}, conversation:`, conversation.id); return NextResponse.json({ success: true,