import { usePrisma } from "../utils/prisma"; // ─── Gruppen-Chat ───────────────────────────────────────────────────────────── export async function getChatMessages(limit = 100) { const db = usePrisma(); return db.chatMessage.findMany({ where: { roomId: null }, orderBy: { createdAt: "asc" }, take: limit, select: { id: true, content: true, createdAt: true, userId: true }, }); } export async function createChatMessage(userId: string, content: string) { const db = usePrisma(); return db.chatMessage.create({ data: { userId, content, roomId: null }, select: { id: true, content: true, createdAt: true, userId: true }, }); } // ─── Direktnachrichten ─────────────────────────────────────────────────────── export async function sendDirectMessage( senderId: string, receiverId: string, content: string, opts?: { replyToId?: string; attachmentUrl?: string; attachmentType?: string; attachmentName?: string; }, ) { const db = usePrisma(); const msg = await db.directMessage.create({ data: { senderId, receiverId, content, replyToId: opts?.replyToId || null, attachmentUrl: opts?.attachmentUrl || null, attachmentType: opts?.attachmentType || null, attachmentName: opts?.attachmentName || null, }, select: { id: true, content: true, createdAt: true, replyToId: true, attachmentUrl: true, attachmentType: true, attachmentName: true, likesCount: true, replyTo: { select: { id: true, senderId: true, content: true }, }, }, }); // Push-Notification (fire-and-forget — blockt Response nicht) void (async () => { try { const { sendChatPush, getDisplayName, truncatePreview } = await import( "../services/push" ); const senderName = await getDisplayName(senderId); // Call-DM-Eintrag (attachmentType='call', attachmentName='::') // → schöne Preview statt leeren String. Bei verpassten/abgelehnten Calls // ist content immer "". let basePreview = content; if (opts?.attachmentType === "call" && opts.attachmentName) { const [, state, durSecStr] = opts.attachmentName.split(":"); const durSec = parseInt(durSecStr ?? "0", 10) || 0; if (state === "unanswered") basePreview = "📞 Verpasster Anruf"; else if (state === "declined") basePreview = "📞 Anruf abgelehnt"; else if (state === "failed") basePreview = "📞 Anruf fehlgeschlagen"; else if (state === "busy") basePreview = "📞 Besetzt"; else if (state === "ended") { const mm = Math.floor(durSec / 60); const ss = String(durSec % 60).padStart(2, "0"); basePreview = `📞 Anruf (${mm}:${ss})`; } else { basePreview = "📞 Anruf"; } } else if (!basePreview) { basePreview = opts?.attachmentType === "audio" ? "🎤 Sprachnachricht" : (opts?.attachmentType === "image" || opts?.attachmentUrl) ? "📷 Foto" : ""; } const preview = truncatePreview(basePreview); console.log(`[dm-push] sender=${senderId} receiver=${receiverId} preview="${preview.slice(0, 30)}"`); await sendChatPush({ receiverId, senderName, preview, data: { type: "dm", targetId: senderId, messageId: msg.id }, }); } catch (err) { console.error("[dm-push] failed:", err); } })(); return msg; } export async function getDmHistory( userId: string, partnerId: string, page = 1, limit = 50, ) { const db = usePrisma(); const offset = (page - 1) * limit; return db.directMessage.findMany({ where: { OR: [ { senderId: userId, receiverId: partnerId }, { senderId: partnerId, receiverId: userId }, ], }, orderBy: { createdAt: "desc" }, skip: offset, take: limit, select: { id: true, senderId: true, receiverId: true, content: true, createdAt: true, readAt: true, replyToId: true, attachmentUrl: true, attachmentType: true, attachmentName: true, likesCount: true, deletedAt: true, reactions: { select: { userId: true, emoji: true }, }, replyTo: { select: { id: true, senderId: true, content: true }, }, }, }); } /** * Toggle einer Emoji-Reaktion auf eine DM (WhatsApp-Verhalten): * - kein Eintrag → Reaktion anlegen * - gleiches Emoji → Reaktion entfernen (toggle off) * - anderes Emoji → Reaktion ersetzen * Eine Reaktion pro User pro Message (PK user+message). */ export async function toggleDmReaction( userId: string, messageId: string, emoji: string, ) { const db = usePrisma(); const existing = await db.directMessageReaction.findUnique({ where: { userId_messageId: { userId, messageId } }, }); if (existing) { if (existing.emoji === emoji) { await db.directMessageReaction.delete({ where: { userId_messageId: { userId, messageId } }, }); return { emoji: null as string | null }; } const updated = await db.directMessageReaction.update({ where: { userId_messageId: { userId, messageId } }, data: { emoji }, }); return { emoji: updated.emoji }; } const created = await db.directMessageReaction.create({ data: { userId, messageId, emoji }, }); return { emoji: created.emoji }; } /** * Soft-Delete einer DM (Tombstone). Nur eigene Nachrichten löschbar. * @returns true wenn gelöscht, false wenn nicht erlaubt/nicht gefunden. */ export async function softDeleteDmMessage(userId: string, messageId: string) { const db = usePrisma(); const res = await db.directMessage.updateMany({ where: { id: messageId, senderId: userId, deletedAt: null }, data: { deletedAt: new Date() }, }); return res.count > 0; } export async function markDmsAsRead(senderId: string, receiverId: string) { const db = usePrisma(); return db.directMessage.updateMany({ where: { senderId, receiverId, readAt: null }, data: { readAt: new Date() }, }); } export type DmConversationRow = { id: string; senderId: string; receiverId: string; content: string; createdAt: Date; readAt: Date | null; attachmentType: string | null; }; export async function getDmConversations(userId: string): Promise { const db = usePrisma(); // Eine Zeile pro Gesprächspartner: die jeweils NEUESTE DM. Postgres // DISTINCT ON (partner) + ORDER BY partner, created_at DESC erledigt das // DB-seitig in EINER Query (index-gestützt), statt 500 Rows zu ziehen und // in JS zu deduplizieren. Spalten werden auf camelCase aliased, damit die // Rows shape-kompatibel zur vorherigen Prisma-Selection bleiben. return db.$queryRaw` SELECT DISTINCT ON (partner_id) id, sender_id AS "senderId", receiver_id AS "receiverId", content, created_at AS "createdAt", read_at AS "readAt", attachment_type AS "attachmentType" FROM ( SELECT *, CASE WHEN sender_id = ${userId}::uuid THEN receiver_id ELSE sender_id END AS partner_id FROM "rebreak"."direct_messages" WHERE sender_id = ${userId}::uuid OR receiver_id = ${userId}::uuid ) sub ORDER BY partner_id, created_at DESC `; } export async function countUnreadDms(receiverId: string) { const db = usePrisma(); const rows = await db.directMessage.findMany({ where: { receiverId, readAt: null }, select: { senderId: true }, }); const byPartner: Record = {}; for (const r of rows) { byPartner[r.senderId] = (byPartner[r.senderId] ?? 0) + 1; } return byPartner; }