From 9e587a23dd00eb81d1dd558feaf075dd2a1be335 Mon Sep 17 00:00:00 2001 From: null Date: Fri, 19 Jun 2026 03:19:58 -0500 Subject: [PATCH] feat: update question thread data source, repository, ViewModel, and Firestore security rules --- .../remote/FirestoreQuestionThreadDataSource.kt | 15 ++++++--------- .../repository/QuestionThreadRepositoryImpl.kt | 5 +++-- .../domain/repository/QuestionThreadRepository.kt | 2 +- .../ui/questions/QuestionThreadViewModel.kt | 2 +- firestore.rules | 8 ++------ 5 files changed, 13 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/app/closer/data/remote/FirestoreQuestionThreadDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreQuestionThreadDataSource.kt index 42691027..0e74c2b4 100644 --- a/app/src/main/java/app/closer/data/remote/FirestoreQuestionThreadDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirestoreQuestionThreadDataSource.kt @@ -35,7 +35,7 @@ class FirestoreQuestionThreadDataSource @Inject constructor(private val db: Fire return snap.documents.firstOrNull()?.id } - suspend fun createThread(coupleId: String, questionId: String, categoryId: String): String { + suspend fun createThread(coupleId: String, questionId: String, categoryId: String, userId: String): String { val now = FieldValue.serverTimestamp() val doc = threadsRef(coupleId).document() doc.set( @@ -44,6 +44,7 @@ class FirestoreQuestionThreadDataSource @Inject constructor(private val db: Fire "categoryId" to categoryId, "status" to QuestionThreadStatus.NOT_STARTED.toFirestoreValue(), "currentIndex" to 0, + "createdByUserId" to userId, "createdAt" to now, "updatedAt" to now ) @@ -62,12 +63,8 @@ class FirestoreQuestionThreadDataSource @Inject constructor(private val db: Fire suspend fun updateThreadStatus(coupleId: String, threadId: String, status: QuestionThreadStatus) { threadsRef(coupleId).document(threadId) - .update( - mapOf( - "status" to status.toFirestoreValue(), - "updatedAt" to FieldValue.serverTimestamp() - ) - ).voidAwait() + .update("status", status.toFirestoreValue()) + .voidAwait() } // ─── Answers ───────────────────────────────────────────────────────────────── @@ -119,7 +116,7 @@ class FirestoreQuestionThreadDataSource @Inject constructor(private val db: Fire .collection(FirestoreCollections.QuestionThreads.MESSAGES) .add( mapOf( - "userId" to message.userId, + "authorUserId" to message.userId, "text" to message.text, "createdAt" to FieldValue.serverTimestamp() ) @@ -222,7 +219,7 @@ class FirestoreQuestionThreadDataSource @Inject constructor(private val db: Fire } private fun DocumentSnapshot.toQuestionMessage(): QuestionMessage? { - val userId = getString("userId") ?: return null + val userId = getString("authorUserId") ?: return null return QuestionMessage( id = id, userId = userId, diff --git a/app/src/main/java/app/closer/data/repository/QuestionThreadRepositoryImpl.kt b/app/src/main/java/app/closer/data/repository/QuestionThreadRepositoryImpl.kt index f1b7eb2b..02978726 100644 --- a/app/src/main/java/app/closer/data/repository/QuestionThreadRepositoryImpl.kt +++ b/app/src/main/java/app/closer/data/repository/QuestionThreadRepositoryImpl.kt @@ -21,9 +21,10 @@ class QuestionThreadRepositoryImpl @Inject constructor( override suspend fun findOrCreateThreadId( coupleId: String, questionId: String, - categoryId: String + categoryId: String, + userId: String ): String = dataSource.findThreadByQuestionId(coupleId, questionId) - ?: dataSource.createThread(coupleId, questionId, categoryId) + ?: dataSource.createThread(coupleId, questionId, categoryId, userId) override fun observeThread(coupleId: String, threadId: String): Flow = dataSource.observeThread(coupleId, threadId) diff --git a/app/src/main/java/app/closer/domain/repository/QuestionThreadRepository.kt b/app/src/main/java/app/closer/domain/repository/QuestionThreadRepository.kt index 967b133a..67fa4ec9 100644 --- a/app/src/main/java/app/closer/domain/repository/QuestionThreadRepository.kt +++ b/app/src/main/java/app/closer/domain/repository/QuestionThreadRepository.kt @@ -7,7 +7,7 @@ import app.closer.domain.model.QuestionThread import kotlinx.coroutines.flow.Flow interface QuestionThreadRepository { - suspend fun findOrCreateThreadId(coupleId: String, questionId: String, categoryId: String): String + suspend fun findOrCreateThreadId(coupleId: String, questionId: String, categoryId: String, userId: String): String fun observeThread(coupleId: String, threadId: String): Flow suspend fun submitAnswer(coupleId: String, threadId: String, userId: String, answer: QuestionAnswer) fun observeAnswers(coupleId: String, threadId: String): Flow> diff --git a/app/src/main/java/app/closer/ui/questions/QuestionThreadViewModel.kt b/app/src/main/java/app/closer/ui/questions/QuestionThreadViewModel.kt index 900bc3a4..d95780c9 100644 --- a/app/src/main/java/app/closer/ui/questions/QuestionThreadViewModel.kt +++ b/app/src/main/java/app/closer/ui/questions/QuestionThreadViewModel.kt @@ -76,7 +76,7 @@ class QuestionThreadViewModel @Inject constructor( } _uiState.update { it.copy(question = question, isLoading = false) } - val threadId = repository.findOrCreateThreadId(coupleId, questionId, question.category) + val threadId = repository.findOrCreateThreadId(coupleId, questionId, question.category, currentUserId) _uiState.update { it.copy(threadId = threadId) } launch { diff --git a/firestore.rules b/firestore.rules index 2847eb09..89616c19 100644 --- a/firestore.rules +++ b/firestore.rules @@ -171,13 +171,9 @@ service cloud.firestore { // - user IDs are immutable (cannot change who is in the couple) // - invite code is immutable (cannot change the code) // - createdAt is immutable (cannot change when the couple was formed) - // - streakCount and lastStreakAt: server-only (via Cloud Functions or admin SDK) - // - All other fields: both members can update normally + // - All other fields (including streakCount and lastAnsweredAt): both members can update allow update: if isCouplesMember(coupleId) - // Check immutable fields haven't changed - && isImmutable(['userIds', 'inviteCode', 'createdAt']) - // streakCount and lastStreakAt must not be modified by clients - && !request.resource.data.diff(resource.data).affectedKeys().hasAny(['streakCount', 'lastStreakAt']); + && isImmutable(['userIds', 'inviteCode', 'createdAt']); // Delete: server-only (admin SDK only). Admin SDK bypasses rules. allow delete: if false;