From 631064fcfe6268e74f02563a80fbdeccd2437f66 Mon Sep 17 00:00:00 2001 From: null Date: Tue, 30 Jun 2026 18:13:56 -0500 Subject: [PATCH] feat(date-memories): add FirestoreDateMemoryDataSource --- .../remote/FirestoreDateMemoryDataSource.kt | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 app/src/main/java/app/closer/data/remote/FirestoreDateMemoryDataSource.kt diff --git a/app/src/main/java/app/closer/data/remote/FirestoreDateMemoryDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreDateMemoryDataSource.kt new file mode 100644 index 00000000..12941762 --- /dev/null +++ b/app/src/main/java/app/closer/data/remote/FirestoreDateMemoryDataSource.kt @@ -0,0 +1,74 @@ +package app.closer.data.remote + +import app.closer.domain.model.DateMemory +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.Query +import com.google.firebase.firestore.SetOptions +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.suspendCancellableCoroutine +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +/** + * Completed-date log for the Date Replay timeline: `couples/{coupleId}/date_history/{id}`. + * PLAINTEXT (date-idea title/category + timestamp — coarse metadata, not private words; the private + * reflection lives E2EE in [FirestoreDateReflectionDataSource]). Doc id = source matchId → idempotent. + */ +@Singleton +class FirestoreDateMemoryDataSource @Inject constructor(private val db: FirebaseFirestore) { + + private fun historyRef(coupleId: String) = + db.collection(FirestoreCollections.COUPLES).document(coupleId) + .collection(FirestoreCollections.Couples.DATE_HISTORY) + + private fun toMemory(d: com.google.firebase.firestore.DocumentSnapshot) = DateMemory( + id = d.id, + dateIdeaId = d.getString("dateIdeaId") ?: "", + title = d.getString("title") ?: "", + category = d.getString("category") ?: "", + completedAt = d.getLong("completedAt") ?: 0L, + addedBy = d.getString("addedBy") ?: "" + ) + + /** Idempotent (merge on doc id = matchId — both partners marking the same date = one record). */ + suspend fun markCompleted(coupleId: String, memory: DateMemory): Unit = + suspendCancellableCoroutine { cont -> + historyRef(coupleId).document(memory.id).set( + mapOf( + "dateIdeaId" to memory.dateIdeaId, + "title" to memory.title, + "category" to memory.category, + "completedAt" to memory.completedAt, + "addedBy" to memory.addedBy + ), + SetOptions.merge() + ).addOnSuccessListener { cont.resume(Unit) }.addOnFailureListener { cont.resumeWithException(it) } + } + + fun observeHistory(coupleId: String): Flow> = callbackFlow { + val reg = historyRef(coupleId) + .orderBy("completedAt", Query.Direction.DESCENDING) + .addSnapshotListener { snap, err -> + if (err != null) { close(err); return@addSnapshotListener } + trySend(snap?.documents?.map(::toMemory) ?: emptyList()) + } + awaitClose { reg.remove() } + } + + suspend fun getHistoryOnce(coupleId: String): List = + suspendCancellableCoroutine { cont -> + historyRef(coupleId).orderBy("completedAt", Query.Direction.DESCENDING).get() + .addOnSuccessListener { snap -> cont.resume(snap.documents.map(::toMemory)) } + .addOnFailureListener { cont.resumeWithException(it) } + } + + suspend fun delete(coupleId: String, id: String): Unit = + suspendCancellableCoroutine { cont -> + historyRef(coupleId).document(id).delete() + .addOnSuccessListener { cont.resume(Unit) }.addOnFailureListener { cont.resumeWithException(it) } + } +}