From 0feb72eaf0285459b84bcb9ef1e620c6c135be83 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 1 Jul 2026 00:21:33 -0500 Subject: [PATCH] feat(home): reveal-waiting art swap, copy polish, extracted computeDailyQuestionState --- .../java/app/closer/ui/home/HomeScreen.kt | 16 ++++- .../java/app/closer/ui/home/HomeViewModel.kt | 70 +++++++++++++------ 2 files changed, 63 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/app/closer/ui/home/HomeScreen.kt b/app/src/main/java/app/closer/ui/home/HomeScreen.kt index eb18e986..73283edc 100644 --- a/app/src/main/java/app/closer/ui/home/HomeScreen.kt +++ b/app/src/main/java/app/closer/ui/home/HomeScreen.kt @@ -1007,7 +1007,17 @@ private fun PrimaryHomeActionCard( ) { val colors = action.tone.actionColors() val isDark = isCloserDarkTheme() - val artRes = homePrimaryArt(action.target) + // The reveal-waiting moment (you answered, then your partner did) gets its own warm couple art so + // "your reveal is waiting" reads as the night's focus. Every other daily-question state keeps the + // default target-based art. + val artRes = if ( + action.target == HomeActionTarget.DailyQuestion && + dailyQuestionState == DailyQuestionState.BOTH_ANSWERED + ) { + R.drawable.illustration_tonight_partner_prompt + } else { + homePrimaryArt(action.target) + } // For daily-question actions, route the CTA through the explicit state handlers // so the same button label maps to the correct next step (answer, remind, @@ -1027,7 +1037,7 @@ private fun PrimaryHomeActionCard( DailyQuestionState.UNANSWERED -> "Tonight's question is ready." DailyQuestionState.USER_ANSWERED_PARTNER_PENDING -> "You showed up tonight. Waiting for your partner." DailyQuestionState.PARTNER_ANSWERED_USER_PENDING -> "Your partner answered. Your turn." - DailyQuestionState.BOTH_ANSWERED -> "Reveal is ready." + DailyQuestionState.BOTH_ANSWERED -> "Your reveal is waiting" DailyQuestionState.REVEALED -> "You opened a conversation tonight." } else -> action.title @@ -1042,7 +1052,7 @@ private fun PrimaryHomeActionCard( DailyQuestionState.PARTNER_ANSWERED_USER_PENDING -> "Answer to unlock the reveal. Your response stays private until you are ready." DailyQuestionState.BOTH_ANSWERED -> - "Both of you answered. Open it together when you are both in the right headspace." + "You both answered — open it together when you're ready." DailyQuestionState.REVEALED -> "You revealed an answer together. What comes next is up to both of you." } diff --git a/app/src/main/java/app/closer/ui/home/HomeViewModel.kt b/app/src/main/java/app/closer/ui/home/HomeViewModel.kt index bc0dcf76..577a8e03 100644 --- a/app/src/main/java/app/closer/ui/home/HomeViewModel.kt +++ b/app/src/main/java/app/closer/ui/home/HomeViewModel.kt @@ -127,6 +127,39 @@ enum class DailyQuestionState { REVEALED } +/** + * Pure derivation of the daily-question home state — extracted so it can be unit-tested directly, + * mirroring the pure-logic style of [HomePriorityEngine]. + * + * The partner is only counted as "answered" when their answer is for THIS question. The partner-answer + * listener keys off *today's date*, not the question id, so a rotated daily question (e.g. across + * midnight while Home is open) could otherwise mark the pair as both-answered for a question the partner + * never answered. A blank [partnerAnsweredQuestionId] (legacy answer docs written before the field + * existed) falls back to plain existence so the common case never regresses. + */ +@androidx.annotation.VisibleForTesting +internal fun computeDailyQuestionState( + questionId: String?, + answeredQuestionIds: Set, + latestAnswer: LocalAnswer?, + hasPartnerAnsweredToday: Boolean, + partnerAnsweredQuestionId: String? +): DailyQuestionState { + val userAnswered = questionId != null && questionId in answeredQuestionIds + val userRevealed = questionId != null && + latestAnswer?.let { it.questionId == questionId && it.isRevealed } == true + val partnerAnswered = hasPartnerAnsweredToday && + (partnerAnsweredQuestionId == questionId || partnerAnsweredQuestionId.isNullOrBlank()) + return when { + questionId == null -> DailyQuestionState.UNANSWERED + userRevealed -> DailyQuestionState.REVEALED + userAnswered && partnerAnswered -> DailyQuestionState.BOTH_ANSWERED + userAnswered -> DailyQuestionState.USER_ANSWERED_PARTNER_PENDING + partnerAnswered -> DailyQuestionState.PARTNER_ANSWERED_USER_PENDING + else -> DailyQuestionState.UNANSWERED + } +} + data class HomeUiState( val isLoading: Boolean = true, val error: String? = null, @@ -600,7 +633,7 @@ class HomeViewModel @Inject constructor( partnerAnswerListener?.remove() partnerAnswerListener = null val cId = coupleId ?: return - val qId = dailyQuestionId ?: return + dailyQuestionId ?: return val uid = authRepository.currentUserId ?: return val partnerId = coupleUserIds?.firstOrNull { it != uid } ?: return @@ -617,33 +650,30 @@ class HomeViewModel @Inject constructor( return@addSnapshotListener } val hasPartnerAnswer = snapshot?.exists() == true + // Capture WHICH question the partner answered (a plaintext routing field on the doc), + // so reveal-ready reflects this question rather than "any answer exists for today" — + // the daily question can rotate. See refreshDailyQuestionState(). + val partnerQuestionId = snapshot?.getString("questionId") _uiState.update { it.copy( hasPartnerAnsweredToday = hasPartnerAnswer, - partnerAnsweredQuestionId = if (hasPartnerAnswer) qId else null + partnerAnsweredQuestionId = if (hasPartnerAnswer) partnerQuestionId else null ).refreshDailyQuestionState().withHomeActions() } } } private fun HomeUiState.refreshDailyQuestionState(): HomeUiState { - val questionId = dailyQuestion?.id - val userAnswered = questionId != null && questionId in answerStats.answeredQuestionIds - val userRevealed = questionId != null && answerStats.latest?.let { latest -> - latest.questionId == questionId && latest.isRevealed - } == true - - val state = when { - questionId == null -> DailyQuestionState.UNANSWERED - userRevealed -> DailyQuestionState.REVEALED - userAnswered && hasPartnerAnsweredToday -> DailyQuestionState.BOTH_ANSWERED - userAnswered -> DailyQuestionState.USER_ANSWERED_PARTNER_PENDING - hasPartnerAnsweredToday -> DailyQuestionState.PARTNER_ANSWERED_USER_PENDING - else -> DailyQuestionState.UNANSWERED - } + val state = computeDailyQuestionState( + questionId = dailyQuestion?.id, + answeredQuestionIds = answerStats.answeredQuestionIds, + latestAnswer = answerStats.latest, + hasPartnerAnsweredToday = hasPartnerAnsweredToday, + partnerAnsweredQuestionId = partnerAnsweredQuestionId + ) return copy( dailyQuestionState = state, - hasRevealedToday = userRevealed + hasRevealedToday = state == DailyQuestionState.REVEALED ) } @@ -720,8 +750,8 @@ class HomeViewModel @Inject constructor( ) Priority.REVEAL_READY -> buildDailyQuestionAction( - title = "Reveal is ready.", - body = "Both of you answered. Open it together when you are both in the right headspace.", + title = "Your reveal is waiting", + body = "You both answered — open it together when you're ready.", cta = "Reveal together" ) @@ -860,7 +890,7 @@ class HomeViewModel @Inject constructor( if (dailyQuestionState == DailyQuestionState.BOTH_ANSWERED) { actions += PendingActionCard( - title = "Reveal is ready", + title = "Your reveal is waiting", subtitle = "Both of you answered tonight. Open it together.", priority = 1, target = HomeActionTarget.AnswerReveal