refactor(home): lift pure action mapper -> HomeActionMapper.kt (VM 867->562)
Part 2b. Moves the ~305 lines of pure HomeUiState action-derivation extensions (refreshDailyQuestionState/withHomeActions internal; toHomeAction/ buildDailyQuestionAction/buildPendingActions/has*/toHomeLabel private) out of HomeViewModel verbatim (de-indented, byte-identical bodies). Verified pure — no this@HomeViewModel/repo refs. VM call-sites unchanged (same-package extensions). compile + full unit suite green. HomeViewModel now 562 lines (from 1030). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
227d141221
commit
7be9a1bf15
|
|
@ -0,0 +1,318 @@
|
|||
package app.closer.ui.home
|
||||
|
||||
import app.closer.ui.home.HomePriorityEngine.Input as PriorityInput
|
||||
import app.closer.ui.home.HomePriorityEngine.Priority
|
||||
|
||||
/**
|
||||
* Pure Home action-derivation logic, lifted verbatim from HomeViewModel as top-level HomeUiState
|
||||
* extensions so it can be unit-tested directly (like HomePriorityEngine). No Firestore/ViewModel
|
||||
* state — every function derives new HomeUiState/HomeAction values from the receiver. The two entry
|
||||
* points the ViewModel calls (refreshDailyQuestionState, withHomeActions) are `internal`; the rest
|
||||
* are file-private helpers.
|
||||
*/
|
||||
|
||||
internal fun HomeUiState.refreshDailyQuestionState(): HomeUiState {
|
||||
val state = computeDailyQuestionState(
|
||||
questionId = dailyQuestion?.id,
|
||||
answeredQuestionIds = answerStats.answeredQuestionIds,
|
||||
latestAnswer = answerStats.latest,
|
||||
hasPartnerAnsweredToday = hasPartnerAnsweredToday,
|
||||
partnerAnsweredQuestionId = partnerAnsweredQuestionId
|
||||
)
|
||||
return copy(
|
||||
dailyQuestionState = state,
|
||||
hasRevealedToday = state == DailyQuestionState.REVEALED
|
||||
)
|
||||
}
|
||||
|
||||
internal fun HomeUiState.withHomeActions(): HomeUiState {
|
||||
if (isLoading || error != null) {
|
||||
return copy(primaryAction = null, secondaryActions = emptyList(), pendingActions = emptyList())
|
||||
}
|
||||
|
||||
val engineInput = PriorityInput(
|
||||
needsCriticalAction = needsRecovery,
|
||||
isPaired = isPaired,
|
||||
needsEncryptionUnlock = needsRecovery,
|
||||
revealReady = dailyQuestionState == DailyQuestionState.BOTH_ANSWERED,
|
||||
partnerAnsweredUserPending = dailyQuestionState == DailyQuestionState.PARTNER_ANSWERED_USER_PENDING,
|
||||
gameWaiting = hasWaitingGame(),
|
||||
challengeWaiting = hasIncompleteChallenge(),
|
||||
dailyQuestionUnanswered = dailyQuestionState == DailyQuestionState.UNANSWERED && dailyQuestion != null,
|
||||
dailyQuestionAwaitingPartner = dailyQuestionState == DailyQuestionState.USER_ANSWERED_PARTNER_PENDING,
|
||||
dailyQuestionRevealed = dailyQuestionState == DailyQuestionState.REVEALED && dailyQuestion != null,
|
||||
weeklyRecapReady = weeklyRecapReady,
|
||||
capsuleUnlocked = hasUnlockedCapsule(),
|
||||
dateReminder = hasUpcomingDate(),
|
||||
dateReflectionPending = hasPendingDateReflection,
|
||||
suggestedPackAvailable = categories.isNotEmpty(),
|
||||
exploreGamesAvailable = categories.isNotEmpty()
|
||||
)
|
||||
val priorityOutput = HomePriorityEngine.compute(engineInput)
|
||||
|
||||
val primary = priorityOutput.primary?.let { toHomeAction(it.priority) }
|
||||
val secondary = priorityOutput.secondary.mapNotNull { toHomeAction(it.priority) }
|
||||
// The primary action already gets the prominent hero card; drop it from the "Waiting for
|
||||
// you" list so the same item isn't surfaced twice (C-HOME-001).
|
||||
val pending = buildPendingActions().filterNot { pending ->
|
||||
pending.target == primary?.target ||
|
||||
(primary?.target == HomeActionTarget.DailyQuestion &&
|
||||
(pending.target == HomeActionTarget.AnswerReveal ||
|
||||
pending.target == HomeActionTarget.DailyQuestion))
|
||||
}
|
||||
|
||||
return copy(
|
||||
primaryAction = primary,
|
||||
secondaryActions = secondary.take(3),
|
||||
pendingActions = pending.take(3)
|
||||
)
|
||||
}
|
||||
|
||||
private fun HomeUiState.toHomeAction(priority: Priority): HomeAction? = when (priority) {
|
||||
Priority.CRITICAL_ACTION ->
|
||||
if (needsRecovery) HomeAction(
|
||||
eyebrow = "Account recovery",
|
||||
title = "Secure your answers before continuing.",
|
||||
body = "A privacy action needs your attention. Complete recovery to keep your shared space safe.",
|
||||
cta = "Start recovery",
|
||||
target = HomeActionTarget.Settings,
|
||||
tone = HomeActionTone.Utility
|
||||
) else null
|
||||
|
||||
Priority.PAIRING_NEEDED -> HomeAction(
|
||||
eyebrow = "1 of 2 connected",
|
||||
title = "A private space for two",
|
||||
body = "Invite your partner to unlock shared reveals, games, streaks, and answers you can both respond to.",
|
||||
cta = "Invite partner",
|
||||
target = HomeActionTarget.InvitePartner,
|
||||
tone = HomeActionTone.Invite
|
||||
)
|
||||
|
||||
Priority.ENCRYPTION_UNLOCK_NEEDED -> HomeAction(
|
||||
eyebrow = "Encryption unlock",
|
||||
title = "Unlock your shared answers.",
|
||||
body = "Your couple's encryption needs to be restored. Complete recovery to keep accessing your answers.",
|
||||
cta = "Recover keys",
|
||||
target = HomeActionTarget.Settings,
|
||||
tone = HomeActionTone.Utility
|
||||
)
|
||||
|
||||
Priority.REVEAL_READY -> buildDailyQuestionAction(
|
||||
title = "Your reveal is waiting",
|
||||
body = "You both answered — open it together when you're ready.",
|
||||
cta = "Reveal together"
|
||||
)
|
||||
|
||||
Priority.PARTNER_ANSWERED_USER_PENDING -> buildDailyQuestionAction(
|
||||
title = "Your partner answered. Your turn.",
|
||||
body = "Answer to unlock the reveal. Your response stays private until you are ready.",
|
||||
cta = "Answer to unlock reveal"
|
||||
)
|
||||
|
||||
// NOTE: GAME_WAITING fires whenever there's an active session this user hasn't finished
|
||||
// (for async games like This or That, completedByUsers stays empty until BOTH finish, so we
|
||||
// cannot assume the partner has played their part here). Keep this copy accurate for every
|
||||
// state — the real-time "X played their part, your turn" nudge is delivered separately by the
|
||||
// push-driven YOUR_TURN GamePromptBanner, which knows the partner actually finished first.
|
||||
Priority.GAME_WAITING -> HomeAction(
|
||||
eyebrow = "Game in progress",
|
||||
title = "Pick up your game.",
|
||||
body = partnerName?.let {
|
||||
"Jump back in to finish your picks and see how you and $it line up."
|
||||
} ?: "Jump back in to finish your picks and see how you two line up.",
|
||||
cta = "Play now",
|
||||
target = HomeActionTarget.Game,
|
||||
tone = HomeActionTone.Ritual,
|
||||
gameRoute = waitingGameRoute
|
||||
)
|
||||
|
||||
Priority.CHALLENGE_WAITING -> HomeAction(
|
||||
eyebrow = "Connection challenge",
|
||||
title = "Today’s challenge step is ready.",
|
||||
body = "Open one small shared action for tonight. It is meant to feel doable, not like homework.",
|
||||
cta = "Open challenge",
|
||||
target = HomeActionTarget.Challenge,
|
||||
tone = HomeActionTone.Ritual
|
||||
)
|
||||
|
||||
Priority.DAILY_QUESTION_UNANSWERED -> buildDailyQuestionAction(
|
||||
title = dailyQuestion?.text ?: "Tonight's question is ready.",
|
||||
body = "Start with one honest answer. You can keep it private or reveal it when the moment feels right.",
|
||||
cta = "Answer privately"
|
||||
)
|
||||
|
||||
// You answered; the reveal waits on your partner. The hero card (PrimaryHomeActionCard) overrides
|
||||
// this title/body and routes the CTA to the gentle-reminder send; this copy/CTA label is what shows
|
||||
// when it renders as a smaller secondary card (a game/challenge is the hero).
|
||||
Priority.DAILY_QUESTION_AWAITING_PARTNER -> buildDailyQuestionAction(
|
||||
title = "You showed up tonight.",
|
||||
body = partnerName?.let { "Your answer stays private until $it answers too — no pressure." }
|
||||
?: "Your answer stays private until your partner answers too — no pressure.",
|
||||
cta = "Send a gentle nudge"
|
||||
)
|
||||
|
||||
// You already revealed today — a low-priority closure card that links to the discussion thread.
|
||||
Priority.DAILY_QUESTION_REVEALED -> buildDailyQuestionAction(
|
||||
title = "You opened a conversation tonight.",
|
||||
body = "Keep it going whenever you're both ready.",
|
||||
cta = "Keep the conversation going"
|
||||
)
|
||||
|
||||
Priority.WEEKLY_RECAP_READY -> HomeAction(
|
||||
eyebrow = "Your week together",
|
||||
title = "Look back at what you built this week.",
|
||||
body = "Reveals, answers, and small rituals are summarized for just the two of you.",
|
||||
cta = "See recap",
|
||||
target = HomeActionTarget.WeeklyRecap,
|
||||
tone = HomeActionTone.Reflection
|
||||
)
|
||||
|
||||
Priority.CAPSULE_UNLOCKED -> HomeAction(
|
||||
eyebrow = "Memory capsule",
|
||||
title = "A saved memory is ready to open.",
|
||||
body = "One of your time capsules unlocked. Open it together and remember why you saved it.",
|
||||
cta = "Open capsule",
|
||||
target = HomeActionTarget.MemoryCapsule,
|
||||
tone = HomeActionTone.Reflection
|
||||
)
|
||||
|
||||
Priority.DATE_REMINDER -> HomeAction(
|
||||
eyebrow = "Date coming up",
|
||||
title = "A planned moment is almost here.",
|
||||
body = "You saved a date idea together. Check the details before the night arrives.",
|
||||
cta = "View date",
|
||||
target = HomeActionTarget.DatePlan,
|
||||
tone = HomeActionTone.Ritual
|
||||
)
|
||||
|
||||
Priority.DATE_REFLECTION_PENDING -> HomeAction(
|
||||
eyebrow = "Date replay",
|
||||
title = partnerName?.let { "Reflect on your date with $it 💭" }
|
||||
?: "Reflect on your date 💭",
|
||||
body = "Capture what the night meant to you. You'll reveal your reflections together when you're both ready.",
|
||||
cta = "Add your reflection",
|
||||
target = HomeActionTarget.DateMemories,
|
||||
tone = HomeActionTone.Reflection
|
||||
)
|
||||
|
||||
Priority.SUGGESTED_PACK -> categories.firstOrNull()?.let { category ->
|
||||
HomeAction(
|
||||
eyebrow = "Suggested pack",
|
||||
title = category.category.displayName.ifBlank { "Question pack" },
|
||||
body = "${category.questionCount} questions for when you want a different doorway into the conversation.",
|
||||
cta = "Open pack",
|
||||
target = HomeActionTarget.QuestionPacks,
|
||||
tone = HomeActionTone.Pack,
|
||||
categoryId = category.category.id
|
||||
)
|
||||
}
|
||||
|
||||
Priority.EXPLORE_GAMES -> HomeAction(
|
||||
eyebrow = "Explore",
|
||||
title = "Try a game together.",
|
||||
body = "Playful ways to connect when you both want something light.",
|
||||
cta = "Browse games",
|
||||
target = HomeActionTarget.QuestionPacks,
|
||||
tone = HomeActionTone.Starter
|
||||
)
|
||||
}
|
||||
|
||||
private fun HomeUiState.buildDailyQuestionAction(
|
||||
title: String,
|
||||
body: String,
|
||||
cta: String
|
||||
): HomeAction = HomeAction(
|
||||
eyebrow = "Your daily question",
|
||||
title = title,
|
||||
body = body,
|
||||
cta = cta,
|
||||
target = HomeActionTarget.DailyQuestion,
|
||||
tone = HomeActionTone.Daily,
|
||||
metric = dailyQuestion?.category?.takeIf { it.isNotBlank() }?.toHomeLabel()
|
||||
)
|
||||
|
||||
private fun HomeUiState.buildPendingActions(): List<PendingActionCard> {
|
||||
if (!isPaired) return emptyList()
|
||||
|
||||
val actions = mutableListOf<PendingActionCard>()
|
||||
|
||||
if (dailyQuestionState == DailyQuestionState.BOTH_ANSWERED) {
|
||||
actions += PendingActionCard(
|
||||
title = "Your reveal is waiting",
|
||||
subtitle = "Both of you answered tonight. Open it together.",
|
||||
priority = 1,
|
||||
target = HomeActionTarget.AnswerReveal
|
||||
)
|
||||
}
|
||||
|
||||
if (dailyQuestionState == DailyQuestionState.PARTNER_ANSWERED_USER_PENDING) {
|
||||
actions += PendingActionCard(
|
||||
title = "Your partner answered",
|
||||
subtitle = "Answer tonight’s question to unlock the reveal.",
|
||||
priority = 2,
|
||||
target = HomeActionTarget.DailyQuestion
|
||||
)
|
||||
}
|
||||
|
||||
if (hasWaitingGame()) {
|
||||
actions += PendingActionCard(
|
||||
title = "Game waiting",
|
||||
subtitle = "Your turn to play a game together.",
|
||||
priority = 3,
|
||||
target = HomeActionTarget.Game
|
||||
)
|
||||
}
|
||||
|
||||
if (hasIncompleteChallenge()) {
|
||||
actions += PendingActionCard(
|
||||
title = "Challenge waiting",
|
||||
subtitle = "Today’s small step is ready for both of you.",
|
||||
priority = 4,
|
||||
target = HomeActionTarget.Challenge
|
||||
)
|
||||
}
|
||||
|
||||
if (hasUpcomingDate()) {
|
||||
actions += PendingActionCard(
|
||||
title = "Date coming up",
|
||||
subtitle = "A planned moment is almost here.",
|
||||
priority = 5,
|
||||
target = HomeActionTarget.DatePlan
|
||||
)
|
||||
}
|
||||
|
||||
if (hasPendingDateReflection) {
|
||||
actions += PendingActionCard(
|
||||
title = "Reflect on your date",
|
||||
subtitle = "Capture the night, then reveal together.",
|
||||
priority = 6,
|
||||
target = HomeActionTarget.DateMemories
|
||||
)
|
||||
}
|
||||
|
||||
if (hasUnlockedCapsule()) {
|
||||
actions += PendingActionCard(
|
||||
title = "Capsule unlocked",
|
||||
subtitle = "A saved memory is ready to open together.",
|
||||
priority = 7,
|
||||
target = HomeActionTarget.MemoryCapsule
|
||||
)
|
||||
}
|
||||
|
||||
return actions.sortedBy { it.priority }
|
||||
}
|
||||
|
||||
private fun HomeUiState.hasWaitingGame(): Boolean = hasWaitingGame
|
||||
|
||||
private fun HomeUiState.hasIncompleteChallenge(): Boolean = hasActiveChallenge
|
||||
|
||||
private fun HomeUiState.hasUpcomingDate(): Boolean = hasUpcomingDatePlan
|
||||
|
||||
private fun HomeUiState.hasUnlockedCapsule(): Boolean = hasUnlockedCapsule
|
||||
|
||||
private fun String.toHomeLabel(): String =
|
||||
split("_", "-")
|
||||
.filter { part -> part.isNotBlank() }
|
||||
// Internal format tokens (e.g. the "mc" in daily_fun_mc) must never face users.
|
||||
.filterNot { part -> part.lowercase() in setOf("mc") }
|
||||
.joinToString(" ") { part -> part.replaceFirstChar { it.uppercaseChar() } }
|
||||
|
|
@ -31,8 +31,6 @@ import com.google.firebase.firestore.FirebaseFirestore
|
|||
import com.google.firebase.functions.FirebaseFunctions
|
||||
import com.google.firebase.functions.FirebaseFunctionsException
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import app.closer.ui.home.HomePriorityEngine.Input as PriorityInput
|
||||
import app.closer.ui.home.HomePriorityEngine.Priority
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
|
|
@ -550,311 +548,6 @@ class HomeViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun HomeUiState.refreshDailyQuestionState(): HomeUiState {
|
||||
val state = computeDailyQuestionState(
|
||||
questionId = dailyQuestion?.id,
|
||||
answeredQuestionIds = answerStats.answeredQuestionIds,
|
||||
latestAnswer = answerStats.latest,
|
||||
hasPartnerAnsweredToday = hasPartnerAnsweredToday,
|
||||
partnerAnsweredQuestionId = partnerAnsweredQuestionId
|
||||
)
|
||||
return copy(
|
||||
dailyQuestionState = state,
|
||||
hasRevealedToday = state == DailyQuestionState.REVEALED
|
||||
)
|
||||
}
|
||||
|
||||
private fun HomeUiState.withHomeActions(): HomeUiState {
|
||||
if (isLoading || error != null) {
|
||||
return copy(primaryAction = null, secondaryActions = emptyList(), pendingActions = emptyList())
|
||||
}
|
||||
|
||||
val engineInput = PriorityInput(
|
||||
needsCriticalAction = needsRecovery,
|
||||
isPaired = isPaired,
|
||||
needsEncryptionUnlock = needsRecovery,
|
||||
revealReady = dailyQuestionState == DailyQuestionState.BOTH_ANSWERED,
|
||||
partnerAnsweredUserPending = dailyQuestionState == DailyQuestionState.PARTNER_ANSWERED_USER_PENDING,
|
||||
gameWaiting = hasWaitingGame(),
|
||||
challengeWaiting = hasIncompleteChallenge(),
|
||||
dailyQuestionUnanswered = dailyQuestionState == DailyQuestionState.UNANSWERED && dailyQuestion != null,
|
||||
dailyQuestionAwaitingPartner = dailyQuestionState == DailyQuestionState.USER_ANSWERED_PARTNER_PENDING,
|
||||
dailyQuestionRevealed = dailyQuestionState == DailyQuestionState.REVEALED && dailyQuestion != null,
|
||||
weeklyRecapReady = weeklyRecapReady,
|
||||
capsuleUnlocked = hasUnlockedCapsule(),
|
||||
dateReminder = hasUpcomingDate(),
|
||||
dateReflectionPending = hasPendingDateReflection,
|
||||
suggestedPackAvailable = categories.isNotEmpty(),
|
||||
exploreGamesAvailable = categories.isNotEmpty()
|
||||
)
|
||||
val priorityOutput = HomePriorityEngine.compute(engineInput)
|
||||
|
||||
val primary = priorityOutput.primary?.let { toHomeAction(it.priority) }
|
||||
val secondary = priorityOutput.secondary.mapNotNull { toHomeAction(it.priority) }
|
||||
// The primary action already gets the prominent hero card; drop it from the "Waiting for
|
||||
// you" list so the same item isn't surfaced twice (C-HOME-001).
|
||||
val pending = buildPendingActions().filterNot { pending ->
|
||||
pending.target == primary?.target ||
|
||||
(primary?.target == HomeActionTarget.DailyQuestion &&
|
||||
(pending.target == HomeActionTarget.AnswerReveal ||
|
||||
pending.target == HomeActionTarget.DailyQuestion))
|
||||
}
|
||||
|
||||
return copy(
|
||||
primaryAction = primary,
|
||||
secondaryActions = secondary.take(3),
|
||||
pendingActions = pending.take(3)
|
||||
)
|
||||
}
|
||||
|
||||
private fun HomeUiState.toHomeAction(priority: Priority): HomeAction? = when (priority) {
|
||||
Priority.CRITICAL_ACTION ->
|
||||
if (needsRecovery) HomeAction(
|
||||
eyebrow = "Account recovery",
|
||||
title = "Secure your answers before continuing.",
|
||||
body = "A privacy action needs your attention. Complete recovery to keep your shared space safe.",
|
||||
cta = "Start recovery",
|
||||
target = HomeActionTarget.Settings,
|
||||
tone = HomeActionTone.Utility
|
||||
) else null
|
||||
|
||||
Priority.PAIRING_NEEDED -> HomeAction(
|
||||
eyebrow = "1 of 2 connected",
|
||||
title = "A private space for two",
|
||||
body = "Invite your partner to unlock shared reveals, games, streaks, and answers you can both respond to.",
|
||||
cta = "Invite partner",
|
||||
target = HomeActionTarget.InvitePartner,
|
||||
tone = HomeActionTone.Invite
|
||||
)
|
||||
|
||||
Priority.ENCRYPTION_UNLOCK_NEEDED -> HomeAction(
|
||||
eyebrow = "Encryption unlock",
|
||||
title = "Unlock your shared answers.",
|
||||
body = "Your couple's encryption needs to be restored. Complete recovery to keep accessing your answers.",
|
||||
cta = "Recover keys",
|
||||
target = HomeActionTarget.Settings,
|
||||
tone = HomeActionTone.Utility
|
||||
)
|
||||
|
||||
Priority.REVEAL_READY -> buildDailyQuestionAction(
|
||||
title = "Your reveal is waiting",
|
||||
body = "You both answered — open it together when you're ready.",
|
||||
cta = "Reveal together"
|
||||
)
|
||||
|
||||
Priority.PARTNER_ANSWERED_USER_PENDING -> buildDailyQuestionAction(
|
||||
title = "Your partner answered. Your turn.",
|
||||
body = "Answer to unlock the reveal. Your response stays private until you are ready.",
|
||||
cta = "Answer to unlock reveal"
|
||||
)
|
||||
|
||||
// NOTE: GAME_WAITING fires whenever there's an active session this user hasn't finished
|
||||
// (for async games like This or That, completedByUsers stays empty until BOTH finish, so we
|
||||
// cannot assume the partner has played their part here). Keep this copy accurate for every
|
||||
// state — the real-time "X played their part, your turn" nudge is delivered separately by the
|
||||
// push-driven YOUR_TURN GamePromptBanner, which knows the partner actually finished first.
|
||||
Priority.GAME_WAITING -> HomeAction(
|
||||
eyebrow = "Game in progress",
|
||||
title = "Pick up your game.",
|
||||
body = partnerName?.let {
|
||||
"Jump back in to finish your picks and see how you and $it line up."
|
||||
} ?: "Jump back in to finish your picks and see how you two line up.",
|
||||
cta = "Play now",
|
||||
target = HomeActionTarget.Game,
|
||||
tone = HomeActionTone.Ritual,
|
||||
gameRoute = waitingGameRoute
|
||||
)
|
||||
|
||||
Priority.CHALLENGE_WAITING -> HomeAction(
|
||||
eyebrow = "Connection challenge",
|
||||
title = "Today’s challenge step is ready.",
|
||||
body = "Open one small shared action for tonight. It is meant to feel doable, not like homework.",
|
||||
cta = "Open challenge",
|
||||
target = HomeActionTarget.Challenge,
|
||||
tone = HomeActionTone.Ritual
|
||||
)
|
||||
|
||||
Priority.DAILY_QUESTION_UNANSWERED -> buildDailyQuestionAction(
|
||||
title = dailyQuestion?.text ?: "Tonight's question is ready.",
|
||||
body = "Start with one honest answer. You can keep it private or reveal it when the moment feels right.",
|
||||
cta = "Answer privately"
|
||||
)
|
||||
|
||||
// You answered; the reveal waits on your partner. The hero card (PrimaryHomeActionCard) overrides
|
||||
// this title/body and routes the CTA to the gentle-reminder send; this copy/CTA label is what shows
|
||||
// when it renders as a smaller secondary card (a game/challenge is the hero).
|
||||
Priority.DAILY_QUESTION_AWAITING_PARTNER -> buildDailyQuestionAction(
|
||||
title = "You showed up tonight.",
|
||||
body = partnerName?.let { "Your answer stays private until $it answers too — no pressure." }
|
||||
?: "Your answer stays private until your partner answers too — no pressure.",
|
||||
cta = "Send a gentle nudge"
|
||||
)
|
||||
|
||||
// You already revealed today — a low-priority closure card that links to the discussion thread.
|
||||
Priority.DAILY_QUESTION_REVEALED -> buildDailyQuestionAction(
|
||||
title = "You opened a conversation tonight.",
|
||||
body = "Keep it going whenever you're both ready.",
|
||||
cta = "Keep the conversation going"
|
||||
)
|
||||
|
||||
Priority.WEEKLY_RECAP_READY -> HomeAction(
|
||||
eyebrow = "Your week together",
|
||||
title = "Look back at what you built this week.",
|
||||
body = "Reveals, answers, and small rituals are summarized for just the two of you.",
|
||||
cta = "See recap",
|
||||
target = HomeActionTarget.WeeklyRecap,
|
||||
tone = HomeActionTone.Reflection
|
||||
)
|
||||
|
||||
Priority.CAPSULE_UNLOCKED -> HomeAction(
|
||||
eyebrow = "Memory capsule",
|
||||
title = "A saved memory is ready to open.",
|
||||
body = "One of your time capsules unlocked. Open it together and remember why you saved it.",
|
||||
cta = "Open capsule",
|
||||
target = HomeActionTarget.MemoryCapsule,
|
||||
tone = HomeActionTone.Reflection
|
||||
)
|
||||
|
||||
Priority.DATE_REMINDER -> HomeAction(
|
||||
eyebrow = "Date coming up",
|
||||
title = "A planned moment is almost here.",
|
||||
body = "You saved a date idea together. Check the details before the night arrives.",
|
||||
cta = "View date",
|
||||
target = HomeActionTarget.DatePlan,
|
||||
tone = HomeActionTone.Ritual
|
||||
)
|
||||
|
||||
Priority.DATE_REFLECTION_PENDING -> HomeAction(
|
||||
eyebrow = "Date replay",
|
||||
title = partnerName?.let { "Reflect on your date with $it 💭" }
|
||||
?: "Reflect on your date 💭",
|
||||
body = "Capture what the night meant to you. You'll reveal your reflections together when you're both ready.",
|
||||
cta = "Add your reflection",
|
||||
target = HomeActionTarget.DateMemories,
|
||||
tone = HomeActionTone.Reflection
|
||||
)
|
||||
|
||||
Priority.SUGGESTED_PACK -> categories.firstOrNull()?.let { category ->
|
||||
HomeAction(
|
||||
eyebrow = "Suggested pack",
|
||||
title = category.category.displayName.ifBlank { "Question pack" },
|
||||
body = "${category.questionCount} questions for when you want a different doorway into the conversation.",
|
||||
cta = "Open pack",
|
||||
target = HomeActionTarget.QuestionPacks,
|
||||
tone = HomeActionTone.Pack,
|
||||
categoryId = category.category.id
|
||||
)
|
||||
}
|
||||
|
||||
Priority.EXPLORE_GAMES -> HomeAction(
|
||||
eyebrow = "Explore",
|
||||
title = "Try a game together.",
|
||||
body = "Playful ways to connect when you both want something light.",
|
||||
cta = "Browse games",
|
||||
target = HomeActionTarget.QuestionPacks,
|
||||
tone = HomeActionTone.Starter
|
||||
)
|
||||
}
|
||||
|
||||
private fun HomeUiState.buildDailyQuestionAction(
|
||||
title: String,
|
||||
body: String,
|
||||
cta: String
|
||||
): HomeAction = HomeAction(
|
||||
eyebrow = "Your daily question",
|
||||
title = title,
|
||||
body = body,
|
||||
cta = cta,
|
||||
target = HomeActionTarget.DailyQuestion,
|
||||
tone = HomeActionTone.Daily,
|
||||
metric = dailyQuestion?.category?.takeIf { it.isNotBlank() }?.toHomeLabel()
|
||||
)
|
||||
|
||||
private fun HomeUiState.buildPendingActions(): List<PendingActionCard> {
|
||||
if (!isPaired) return emptyList()
|
||||
|
||||
val actions = mutableListOf<PendingActionCard>()
|
||||
|
||||
if (dailyQuestionState == DailyQuestionState.BOTH_ANSWERED) {
|
||||
actions += PendingActionCard(
|
||||
title = "Your reveal is waiting",
|
||||
subtitle = "Both of you answered tonight. Open it together.",
|
||||
priority = 1,
|
||||
target = HomeActionTarget.AnswerReveal
|
||||
)
|
||||
}
|
||||
|
||||
if (dailyQuestionState == DailyQuestionState.PARTNER_ANSWERED_USER_PENDING) {
|
||||
actions += PendingActionCard(
|
||||
title = "Your partner answered",
|
||||
subtitle = "Answer tonight’s question to unlock the reveal.",
|
||||
priority = 2,
|
||||
target = HomeActionTarget.DailyQuestion
|
||||
)
|
||||
}
|
||||
|
||||
if (hasWaitingGame()) {
|
||||
actions += PendingActionCard(
|
||||
title = "Game waiting",
|
||||
subtitle = "Your turn to play a game together.",
|
||||
priority = 3,
|
||||
target = HomeActionTarget.Game
|
||||
)
|
||||
}
|
||||
|
||||
if (hasIncompleteChallenge()) {
|
||||
actions += PendingActionCard(
|
||||
title = "Challenge waiting",
|
||||
subtitle = "Today’s small step is ready for both of you.",
|
||||
priority = 4,
|
||||
target = HomeActionTarget.Challenge
|
||||
)
|
||||
}
|
||||
|
||||
if (hasUpcomingDate()) {
|
||||
actions += PendingActionCard(
|
||||
title = "Date coming up",
|
||||
subtitle = "A planned moment is almost here.",
|
||||
priority = 5,
|
||||
target = HomeActionTarget.DatePlan
|
||||
)
|
||||
}
|
||||
|
||||
if (hasPendingDateReflection) {
|
||||
actions += PendingActionCard(
|
||||
title = "Reflect on your date",
|
||||
subtitle = "Capture the night, then reveal together.",
|
||||
priority = 6,
|
||||
target = HomeActionTarget.DateMemories
|
||||
)
|
||||
}
|
||||
|
||||
if (hasUnlockedCapsule()) {
|
||||
actions += PendingActionCard(
|
||||
title = "Capsule unlocked",
|
||||
subtitle = "A saved memory is ready to open together.",
|
||||
priority = 7,
|
||||
target = HomeActionTarget.MemoryCapsule
|
||||
)
|
||||
}
|
||||
|
||||
return actions.sortedBy { it.priority }
|
||||
}
|
||||
|
||||
private fun HomeUiState.hasWaitingGame(): Boolean = hasWaitingGame
|
||||
|
||||
private fun HomeUiState.hasIncompleteChallenge(): Boolean = hasActiveChallenge
|
||||
|
||||
private fun HomeUiState.hasUpcomingDate(): Boolean = hasUpcomingDatePlan
|
||||
|
||||
private fun HomeUiState.hasUnlockedCapsule(): Boolean = hasUnlockedCapsule
|
||||
|
||||
private fun String.toHomeLabel(): String =
|
||||
split("_", "-")
|
||||
.filter { part -> part.isNotBlank() }
|
||||
// Internal format tokens (e.g. the "mc" in daily_fun_mc) must never face users.
|
||||
.filterNot { part -> part.lowercase() in setOf("mc") }
|
||||
.joinToString(" ") { part -> part.replaceFirstChar { it.uppercaseChar() } }
|
||||
|
||||
companion object {
|
||||
private const val TAG = "HomeViewModel"
|
||||
|
|
|
|||
Loading…
Reference in New Issue