feat(backup): add backup record reads to FirestoreConversationDataSource (getConversationsForBackup, getBackupRecords)

This commit is contained in:
null 2026-06-30 20:42:34 -05:00
parent 522823f739
commit 909d261b6c
1 changed files with 53 additions and 0 deletions

View File

@ -2,6 +2,7 @@ package app.closer.data.remote
import app.closer.crypto.CoupleEncryptionManager import app.closer.crypto.CoupleEncryptionManager
import app.closer.crypto.FieldEncryptor import app.closer.crypto.FieldEncryptor
import app.closer.domain.model.BackupMessageRecord
import app.closer.domain.model.Conversation import app.closer.domain.model.Conversation
import app.closer.domain.model.QuestionMessage import app.closer.domain.model.QuestionMessage
import com.google.firebase.firestore.DocumentSnapshot 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() 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<Pair<String, String>> =
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<BackupMessageRecord> = 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<String, String>).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 ──────────────────────────────────────────────────── // ─── Mappers / await helpers ────────────────────────────────────────────────────
private fun DocumentSnapshot.toConversation( private fun DocumentSnapshot.toConversation(