feat(home): reveal-waiting art swap, copy polish, extracted computeDailyQuestionState

This commit is contained in:
null 2026-07-01 00:21:33 -05:00
parent b15e696388
commit 0feb72eaf0
2 changed files with 63 additions and 23 deletions

View File

@ -1007,7 +1007,17 @@ private fun PrimaryHomeActionCard(
) { ) {
val colors = action.tone.actionColors() val colors = action.tone.actionColors()
val isDark = isCloserDarkTheme() 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 // 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, // 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.UNANSWERED -> "Tonight's question is ready."
DailyQuestionState.USER_ANSWERED_PARTNER_PENDING -> "You showed up tonight. Waiting for your partner." DailyQuestionState.USER_ANSWERED_PARTNER_PENDING -> "You showed up tonight. Waiting for your partner."
DailyQuestionState.PARTNER_ANSWERED_USER_PENDING -> "Your partner answered. Your turn." 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." DailyQuestionState.REVEALED -> "You opened a conversation tonight."
} }
else -> action.title else -> action.title
@ -1042,7 +1052,7 @@ private fun PrimaryHomeActionCard(
DailyQuestionState.PARTNER_ANSWERED_USER_PENDING -> DailyQuestionState.PARTNER_ANSWERED_USER_PENDING ->
"Answer to unlock the reveal. Your response stays private until you are ready." "Answer to unlock the reveal. Your response stays private until you are ready."
DailyQuestionState.BOTH_ANSWERED -> 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 -> DailyQuestionState.REVEALED ->
"You revealed an answer together. What comes next is up to both of you." "You revealed an answer together. What comes next is up to both of you."
} }

View File

@ -127,6 +127,39 @@ enum class DailyQuestionState {
REVEALED 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<String>,
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( data class HomeUiState(
val isLoading: Boolean = true, val isLoading: Boolean = true,
val error: String? = null, val error: String? = null,
@ -600,7 +633,7 @@ class HomeViewModel @Inject constructor(
partnerAnswerListener?.remove() partnerAnswerListener?.remove()
partnerAnswerListener = null partnerAnswerListener = null
val cId = coupleId ?: return val cId = coupleId ?: return
val qId = dailyQuestionId ?: return dailyQuestionId ?: return
val uid = authRepository.currentUserId ?: return val uid = authRepository.currentUserId ?: return
val partnerId = coupleUserIds?.firstOrNull { it != uid } ?: return val partnerId = coupleUserIds?.firstOrNull { it != uid } ?: return
@ -617,33 +650,30 @@ class HomeViewModel @Inject constructor(
return@addSnapshotListener return@addSnapshotListener
} }
val hasPartnerAnswer = snapshot?.exists() == true 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 { _uiState.update {
it.copy( it.copy(
hasPartnerAnsweredToday = hasPartnerAnswer, hasPartnerAnsweredToday = hasPartnerAnswer,
partnerAnsweredQuestionId = if (hasPartnerAnswer) qId else null partnerAnsweredQuestionId = if (hasPartnerAnswer) partnerQuestionId else null
).refreshDailyQuestionState().withHomeActions() ).refreshDailyQuestionState().withHomeActions()
} }
} }
} }
private fun HomeUiState.refreshDailyQuestionState(): HomeUiState { private fun HomeUiState.refreshDailyQuestionState(): HomeUiState {
val questionId = dailyQuestion?.id val state = computeDailyQuestionState(
val userAnswered = questionId != null && questionId in answerStats.answeredQuestionIds questionId = dailyQuestion?.id,
val userRevealed = questionId != null && answerStats.latest?.let { latest -> answeredQuestionIds = answerStats.answeredQuestionIds,
latest.questionId == questionId && latest.isRevealed latestAnswer = answerStats.latest,
} == true hasPartnerAnsweredToday = hasPartnerAnsweredToday,
partnerAnsweredQuestionId = partnerAnsweredQuestionId
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
}
return copy( return copy(
dailyQuestionState = state, dailyQuestionState = state,
hasRevealedToday = userRevealed hasRevealedToday = state == DailyQuestionState.REVEALED
) )
} }
@ -720,8 +750,8 @@ class HomeViewModel @Inject constructor(
) )
Priority.REVEAL_READY -> buildDailyQuestionAction( Priority.REVEAL_READY -> buildDailyQuestionAction(
title = "Reveal is ready.", title = "Your reveal is waiting",
body = "Both of you answered. Open it together when you are both in the right headspace.", body = "You both answered — open it together when you're ready.",
cta = "Reveal together" cta = "Reveal together"
) )
@ -860,7 +890,7 @@ class HomeViewModel @Inject constructor(
if (dailyQuestionState == DailyQuestionState.BOTH_ANSWERED) { if (dailyQuestionState == DailyQuestionState.BOTH_ANSWERED) {
actions += PendingActionCard( actions += PendingActionCard(
title = "Reveal is ready", title = "Your reveal is waiting",
subtitle = "Both of you answered tonight. Open it together.", subtitle = "Both of you answered tonight. Open it together.",
priority = 1, priority = 1,
target = HomeActionTarget.AnswerReveal target = HomeActionTarget.AnswerReveal