116 lines
4.0 KiB
TypeScript
116 lines
4.0 KiB
TypeScript
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 conversation (the couple chat or a per-question discussion).
|
|
*
|
|
* Path: couples/{coupleId}/conversations/{conversationId}/messages/{messageId}
|
|
*
|
|
* Respects the recipient's `notifChatMessage` preference (default: enabled).
|
|
*/
|
|
export const onMessageWritten = functions.firestore
|
|
.document('couples/{coupleId}/conversations/{conversationId}/messages/{messageId}')
|
|
.onCreate(async (snap, context) => {
|
|
const { coupleId, conversationId, messageId } = context.params as {
|
|
coupleId: string
|
|
conversationId: string
|
|
messageId: string
|
|
}
|
|
|
|
const db = admin.firestore()
|
|
const messageData = snap.data() as Partial<Record<string, unknown>>
|
|
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
|
|
}
|
|
|
|
// The recipient sees the message from the author (their partner), so surface the author's
|
|
// photo/name — the in-app chat bubble uses sender_avatar_url to show the partner's face.
|
|
const authorDoc = await db.collection('users').doc(authorId).get()
|
|
const authorPhotoUrl = (authorDoc.data()?.photoUrl as string | undefined) ?? ''
|
|
const authorName = (authorDoc.data()?.displayName as string | undefined) ?? ''
|
|
|
|
const payload: admin.messaging.MessagingPayload = {
|
|
notification: {
|
|
title: authorName ? `${authorName} sent a message` : 'Your partner sent a message',
|
|
body: 'Tap to read and reply.',
|
|
},
|
|
data: {
|
|
type: 'chat_message',
|
|
couple_id: coupleId,
|
|
conversation_id: conversationId,
|
|
...(authorPhotoUrl ? { sender_avatar_url: authorPhotoUrl } : {}),
|
|
},
|
|
}
|
|
|
|
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 conversation ${conversationId} in couple ${coupleId}`
|
|
)
|
|
})
|