Store chat messages locally for sender and recipient
Messages sent to remote users are now stored locally for the sender, and incoming remote messages are stored for the recipient, ensuring both parties see their message history in the UI. The implementation creates or updates conversations as needed and marks messages as delivered when appropriate.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { db } from '@/db';
|
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 { requireSignedAction } from '@/lib/auth/verify-signature';
|
||||||
import { eq, and } from 'drizzle-orm';
|
import { eq, and } from 'drizzle-orm';
|
||||||
import { signedFetch } from '@/lib/api/signed-fetch';
|
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`;
|
const remoteUrl = `https://${targetNodeDomain}/api/swarm/chat/inbox`;
|
||||||
|
|
||||||
try {
|
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, {
|
const response = await fetch(remoteUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -140,26 +180,14 @@ export async function POST(request: NextRequest) {
|
|||||||
}, { status: response.status });
|
}, { status: response.status });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create local conversation for sender if we have recipient handle
|
// Mark message as delivered since remote accepted it
|
||||||
if (recipientHandle) {
|
await db.update(chatMessages)
|
||||||
const existingConv = await db.query.chatConversations.findFirst({
|
.set({ deliveredAt: new Date() })
|
||||||
where: and(
|
.where(eq(chatMessages.id, newMessage.id));
|
||||||
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]'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
} catch (error: any) {
|
||||||
console.error('[Chat Send] Remote delivery error:', error);
|
console.error('[Chat Send] Remote delivery error:', error);
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Recipient not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Recipient not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import chatInbox schema
|
// Import schemas
|
||||||
const { chatInbox } = await import('@/db/schema');
|
const { chatInbox, chatConversations, chatMessages } = await import('@/db/schema');
|
||||||
|
|
||||||
// Store in V2 inbox
|
// Store in V2 inbox
|
||||||
await db.insert(chatInbox).values({
|
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)
|
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({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user