import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' /** * Firestore trigger that notifies the other partner when a chat message is * sent in a question thread. * * Path: couples/{coupleId}/question_threads/{threadId}/messages/{messageId} * * Respects the recipient's `notifChatMessage` preference (default: enabled). */ export const onMessageWritten = functions.firestore .document('couples/{coupleId}/question_threads/{threadId}/messages/{messageId}') .onCreate(async (snap, context) => { const { coupleId, threadId, messageId } = context.params as { coupleId: string threadId: string messageId: string } const db = admin.firestore() const messageData = snap.data() as Partial> const authorId = typeof messageData.authorUserId === 'string' ? messageData.authorUserId : null if (!authorId) { console.warn(`[onMessageWritten] no authorUserId on message ${messageId}`) return } const coupleDoc = await db.collection('couples').doc(coupleId).get() if (!coupleDoc.exists) { console.warn(`[onMessageWritten] couple ${coupleId} not found`) return } const userIds = (coupleDoc.data()?.userIds ?? []) as string[] const partnerId = userIds.find((uid) => uid !== authorId) if (!partnerId) { console.warn(`[onMessageWritten] no partner found for couple ${coupleId}`) return } const partnerUserDoc = await db.collection('users').doc(partnerId).get() // Respect the partner's notification preference (opt-out; default is enabled). const notifEnabled = partnerUserDoc.data()?.notifChatMessage if (notifEnabled === false) { console.log(`[onMessageWritten] partner ${partnerId} has chat notifications off`) return } const tokens: string[] = [] if (partnerUserDoc.exists) { const legacyToken = partnerUserDoc.data()?.fcmToken if (typeof legacyToken === 'string' && legacyToken.length > 0) { tokens.push(legacyToken) } } const tokenSnapshot = await db .collection('users') .doc(partnerId) .collection('fcmTokens') .get() tokenSnapshot.docs.forEach((doc) => { const t = doc.data()?.token if (typeof t === 'string' && t.length > 0 && !tokens.includes(t)) { tokens.push(t) } }) if (tokens.length === 0) { console.log(`[onMessageWritten] no FCM tokens for partner ${partnerId}`) return } const payload: admin.messaging.MessagingPayload = { notification: { title: 'Your partner sent a message', body: 'Tap to read and reply.', }, data: { type: 'chat_message', coupleId, threadId, }, } const sendResults = await Promise.allSettled( tokens.map((token) => admin.messaging().send({ ...payload, token } as admin.messaging.Message) ) ) const failures: string[] = [] sendResults.forEach((result, index) => { if (result.status === 'rejected') { failures.push(`${tokens[index]}: ${String(result.reason)}`) } }) if (failures.length > 0) { console.error(`[onMessageWritten] some notifications failed:`, failures) } console.log( `[onMessageWritten] notified partner ${partnerId} for thread ${threadId} in couple ${coupleId}` ) })