From 909d261b6c6b847e1dfa798ea9849ce6f7b6fae9 Mon Sep 17 00:00:00 2001 From: null Date: Tue, 30 Jun 2026 20:42:34 -0500 Subject: [PATCH] feat(backup): add backup record reads to FirestoreConversationDataSource (getConversationsForBackup, getBackupRecords) --- .../remote/FirestoreConversationDataSource.kt | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/app/src/main/java/app/closer/data/remote/FirestoreConversationDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreConversationDataSource.kt index df4b0131..686aefb9 100644 --- a/app/src/main/java/app/closer/data/remote/FirestoreConversationDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirestoreConversationDataSource.kt @@ -2,6 +2,7 @@ package app.closer.data.remote import app.closer.crypto.CoupleEncryptionManager import app.closer.crypto.FieldEncryptor +import app.closer.domain.model.BackupMessageRecord import app.closer.domain.model.Conversation import app.closer.domain.model.QuestionMessage import com.google.firebase.firestore.DocumentSnapshot @@ -232,6 +233,58 @@ class FirestoreConversationDataSource @Inject constructor( return runCatching { aead.decrypt(cipher, coupleId.toByteArray(Charsets.UTF_8)) }.getOrNull() } + // ─── Backup reads (source of truth for the encrypted backup) ───────────────────── + + /** All conversations for the couple as (id, type) — `main` + per-question threads. */ + suspend fun getConversationsForBackup(coupleId: String): List> = + runCatching { + conversationsRef(coupleId).get().await().documents.map { + it.id to (it.getString("type") ?: "main") + } + }.getOrDefault(emptyList()) + + /** + * Backup records for one conversation with a resolved `createdAt` strictly after [afterCreatedAt] + * (0 = from the beginning). Pending-write docs (null server timestamp) are skipped so the cursor + * never straddles an unresolved message. Includes tombstones + reactions for fidelity. + */ + suspend fun getBackupRecords( + coupleId: String, + conversationId: String, + conversationType: String, + afterCreatedAt: Long + ): List = runCatching { + var query: Query = messagesRef(coupleId, conversationId).orderBy("createdAt", Query.Direction.ASCENDING) + if (afterCreatedAt > 0L) { + query = query.whereGreaterThan("createdAt", com.google.firebase.Timestamp(java.util.Date(afterCreatedAt))) + } + query.get().await().documents.mapNotNull { it.toBackupRecord(conversationId, conversationType) } + }.getOrDefault(emptyList()) + + private fun DocumentSnapshot.toBackupRecord( + conversationId: String, + conversationType: String + ): BackupMessageRecord? { + // Skip unresolved server timestamps — back them up once they settle. + val createdAt = getTimestamp("createdAt")?.toDate()?.time ?: return null + @Suppress("UNCHECKED_CAST") + val reactions = (get("reactions") as? Map).orEmpty() + val type = getString("type") ?: "text" + return BackupMessageRecord( + messageId = id, + conversationId = conversationId, + conversationType = conversationType, + authorUserId = getString("authorUserId") ?: "", + type = type, + encText = if (type == "text") getString("text") else null, + mediaUrl = getString("mediaUrl"), + durationMs = getLong("durationMs"), + createdAt = createdAt, + deleted = getBoolean("deleted") ?: false, + reactions = reactions + ) + } + // ─── Mappers / await helpers ──────────────────────────────────────────────────── private fun DocumentSnapshot.toConversation(