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:
null 2026-07-11 20:40:31 -05:00
parent 227d141221
commit 7be9a1bf15
2 changed files with 318 additions and 307 deletions

View File

@ -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 = "Todays 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 tonights 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 = "Todays 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() } }

View File

@ -31,8 +31,6 @@ import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.functions.FirebaseFunctions import com.google.firebase.functions.FirebaseFunctions
import com.google.firebase.functions.FirebaseFunctionsException import com.google.firebase.functions.FirebaseFunctionsException
import dagger.hilt.android.lifecycle.HiltViewModel 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.DayOfWeek
import java.time.LocalDate import java.time.LocalDate
import java.time.ZoneId 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 = "Todays 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 tonights 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 = "Todays 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 { companion object {
private const val TAG = "HomeViewModel" private const val TAG = "HomeViewModel"