chahinebrini 38df6fc79d feat(chat): push notifications for DMs + rooms
Backend:
- Prisma PushToken model + chat_push_enabled flag on profiles
- Migration 20260530_add_push_tokens (push_tokens table + profile flag)
- Service sendChatPush with expo-server-sdk (auto-disable invalid tokens)
- Fire-and-forget push trigger in sendDirectMessage + createRoomMessage
- API POST /users/me/push-token (upsert) + DELETE (soft-disable)

Client (rebreak-native):
- usePushTokenRegistration hook: permission, getExpoPushTokenAsync,
  Android channel 'chat', POST to backend; idempotent per session
- Notification tap deep-link: dm -> /dm?userId, room -> /room?roomId

Deploy:
- run_quiet spinner for silent altool/xcodebuild/gradle phases
- Release-notes pipeline (--notes flag / NEXT_RELEASE.md / interactive)
  archived to CHANGELOG.md, printed with ASC + Play Console links
- Default version bump ON (--no-bump opt-out), build cleanup
- NEXT_RELEASE.md with push-notification release note
2026-05-30 08:16:45 +02:00

156 lines
4.1 KiB
TypeScript

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 () => {
const { sendChatPush, getDisplayName, truncatePreview } = await import(
"../services/push"
);
const senderName = await getDisplayName(senderId);
await sendChatPush({
receiverId,
senderName,
preview: truncatePreview(content || (opts?.attachmentUrl ? "📎 Anhang" : "")),
data: { type: "dm", targetId: senderId, messageId: msg.id },
});
})();
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,
replyTo: {
select: { id: true, senderId: true, content: true },
},
},
});
}
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 async function getDmConversations(userId: string) {
const db = usePrisma();
// Alle DMs als Sender oder Empfänger, neueste zuerst
return db.directMessage.findMany({
where: {
OR: [{ senderId: userId }, { receiverId: userId }],
},
orderBy: { createdAt: "desc" },
take: 500,
select: {
id: true,
senderId: true,
receiverId: true,
content: true,
createdAt: true,
readAt: true,
},
});
}
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<string, number> = {};
for (const r of rows) {
byPartner[r.senderId] = (byPartner[r.senderId] ?? 0) + 1;
}
return byPartner;
}