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 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."
}

View File

@ -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<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(
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