feat: capsule/challenge data sources, game screens, wheel history + viewmodel
This commit is contained in:
parent
17403b1a75
commit
c56dd53edd
|
|
@ -26,13 +26,23 @@ class FirestoreCapsuleDataSource @Inject constructor(
|
||||||
.document(coupleId)
|
.document(coupleId)
|
||||||
.collection(FirestoreCollections.Couples.CAPSULES)
|
.collection(FirestoreCollections.Couples.CAPSULES)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts a field value.
|
||||||
|
* - Plaintext (unencrypted legacy value): returned as-is for backward compat.
|
||||||
|
* - Encrypted but key unavailable: returns null so the UI can show a placeholder.
|
||||||
|
* - Decryption failure: returns null rather than leaking a garbled ciphertext.
|
||||||
|
*/
|
||||||
private fun decryptField(value: String?, coupleId: String): String? {
|
private fun decryptField(value: String?, coupleId: String): String? {
|
||||||
if (value == null) return null
|
if (value == null) return null
|
||||||
val aead = encryptionManager.aeadFor(coupleId)
|
if (!fieldEncryptor.isEncrypted(value)) return value // legacy plaintext
|
||||||
|
val aead = encryptionManager.aeadFor(coupleId) ?: run {
|
||||||
|
Log.w(TAG, "key unavailable for $coupleId, masking encrypted field")
|
||||||
|
return null
|
||||||
|
}
|
||||||
return runCatching { fieldEncryptor.decrypt(value, aead, coupleId) }
|
return runCatching { fieldEncryptor.decrypt(value, aead, coupleId) }
|
||||||
.getOrElse { e ->
|
.getOrElse { e ->
|
||||||
Log.w(TAG, "decrypt failed, returning raw: ${e.message}")
|
Log.w(TAG, "decrypt failed: ${e.message}")
|
||||||
value
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,8 +54,8 @@ class FirestoreCapsuleDataSource @Inject constructor(
|
||||||
id = doc.id,
|
id = doc.id,
|
||||||
coupleId = coupleId,
|
coupleId = coupleId,
|
||||||
authorId = doc.getString("authorId") ?: "",
|
authorId = doc.getString("authorId") ?: "",
|
||||||
title = decryptField(rawTitle, coupleId) ?: rawTitle,
|
title = decryptField(rawTitle, coupleId) ?: "— Encrypted —",
|
||||||
content = decryptField(rawContent, coupleId) ?: rawContent,
|
content = decryptField(rawContent, coupleId) ?: "— Encrypted —",
|
||||||
promptUsed = decryptField(rawPrompt, coupleId),
|
promptUsed = decryptField(rawPrompt, coupleId),
|
||||||
unlockAt = doc.getLong("unlockAt") ?: 0L,
|
unlockAt = doc.getLong("unlockAt") ?: 0L,
|
||||||
createdAt = doc.getLong("createdAt") ?: 0L,
|
createdAt = doc.getLong("createdAt") ?: 0L,
|
||||||
|
|
@ -65,11 +75,17 @@ class FirestoreCapsuleDataSource @Inject constructor(
|
||||||
awaitClose { reg.remove() }
|
awaitClose { reg.remove() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a capsule. Requires the couple encryption key to be available — throws
|
||||||
|
* [IllegalStateException] if the key is missing so the caller can surface an error
|
||||||
|
* rather than silently storing plaintext.
|
||||||
|
*/
|
||||||
suspend fun createCapsule(capsule: TimeCapsule): String {
|
suspend fun createCapsule(capsule: TimeCapsule): String {
|
||||||
val aead = encryptionManager.aeadFor(capsule.coupleId)
|
val aead = encryptionManager.aeadFor(capsule.coupleId)
|
||||||
val encTitle = aead?.let { fieldEncryptor.encrypt(capsule.title, it, capsule.coupleId) } ?: capsule.title
|
?: throw IllegalStateException("Encryption key not ready. Please try again in a moment.")
|
||||||
val encContent = aead?.let { fieldEncryptor.encrypt(capsule.content, it, capsule.coupleId) } ?: capsule.content
|
val encTitle = fieldEncryptor.encrypt(capsule.title, aead, capsule.coupleId)
|
||||||
val encPrompt = aead?.let { fieldEncryptor.encryptNullable(capsule.promptUsed, it, capsule.coupleId) } ?: capsule.promptUsed
|
val encContent = fieldEncryptor.encrypt(capsule.content, aead, capsule.coupleId)
|
||||||
|
val encPrompt = fieldEncryptor.encryptNullable(capsule.promptUsed, aead, capsule.coupleId)
|
||||||
|
|
||||||
val ref = col(capsule.coupleId).document()
|
val ref = col(capsule.coupleId).document()
|
||||||
ref.set(
|
ref.set(
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,18 @@ class FirestoreChallengeDataSource @Inject constructor(private val db: FirebaseF
|
||||||
.document(coupleId)
|
.document(coupleId)
|
||||||
.collection(FirestoreCollections.Couples.CHALLENGES)
|
.collection(FirestoreCollections.Couples.CHALLENGES)
|
||||||
|
|
||||||
|
suspend fun getCompletedChallengeIds(coupleId: String): List<Pair<String, Long>> =
|
||||||
|
challengesCol(coupleId)
|
||||||
|
.whereEqualTo("status", "completed")
|
||||||
|
.get()
|
||||||
|
.await()
|
||||||
|
.documents
|
||||||
|
.mapNotNull { doc ->
|
||||||
|
val id = doc.getString("challengeId") ?: return@mapNotNull null
|
||||||
|
val startedAt = doc.getLong("startedAt") ?: 0L
|
||||||
|
Pair(id, startedAt)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getActiveChallengeId(coupleId: String): String? =
|
suspend fun getActiveChallengeId(coupleId: String): String? =
|
||||||
challengesCol(coupleId)
|
challengesCol(coupleId)
|
||||||
.whereEqualTo("status", "active")
|
.whereEqualTo("status", "active")
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ class ConnectionChallengesViewModel @Inject constructor(
|
||||||
private fun load() {
|
private fun load() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val uid = authRepository.currentUserId ?: run {
|
val uid = authRepository.currentUserId ?: run {
|
||||||
_uiState.update { it.copy(phase = ChallengesPhase.PICK) }
|
_uiState.update { it.copy(navigateTo = AppRoute.PLAY) }
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
val couple = coupleRepository.getCoupleForUser(uid) ?: run {
|
val couple = coupleRepository.getCoupleForUser(uid) ?: run {
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,11 @@ class DesireSyncViewModel @Inject constructor(
|
||||||
val sId = sessionId ?: return
|
val sId = sessionId ?: return
|
||||||
val uid = userId ?: return
|
val uid = userId ?: return
|
||||||
runCatching { dataSource.submitAnswers(cId, sId, uid, answers) }
|
runCatching { dataSource.submitAnswers(cId, sId, uid, answers) }
|
||||||
.onFailure { Log.w(TAG, "Could not submit answers", it) }
|
.onFailure { e ->
|
||||||
|
submitted = false
|
||||||
|
Log.w(TAG, "Could not submit answers", e)
|
||||||
|
_uiState.update { it.copy(phase = DesireSyncPhase.ERROR, error = "Couldn't save your answers. Check your connection and go back to try again.") }
|
||||||
|
}
|
||||||
// The observer flips WAITING → REVEAL once the partner's answers land.
|
// The observer flips WAITING → REVEAL once the partner's answers land.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -295,7 +295,11 @@ class HowWellViewModel @Inject constructor(
|
||||||
val sId = sessionId ?: return
|
val sId = sessionId ?: return
|
||||||
val uid = userId ?: return
|
val uid = userId ?: return
|
||||||
runCatching { dataSource.submitAnswers(cId, sId, uid, myAnswers.toList()) }
|
runCatching { dataSource.submitAnswers(cId, sId, uid, myAnswers.toList()) }
|
||||||
.onFailure { Log.w(TAG, "Could not submit answers", it) }
|
.onFailure { e ->
|
||||||
|
submitted = false
|
||||||
|
Log.w(TAG, "Could not submit answers", e)
|
||||||
|
_uiState.update { it.copy(phase = HowWellPhase.ERROR, error = "Couldn't save your answers. Check your connection and go back to try again.") }
|
||||||
|
}
|
||||||
// The observer flips WAITING → COMPLETE once the partner's answers land.
|
// The observer flips WAITING → COMPLETE once the partner's answers land.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -227,9 +227,10 @@ class MemoryLaneViewModel @Inject constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
runCatching { capsuleDataSource.createCapsule(capsule) }
|
runCatching { capsuleDataSource.createCapsule(capsule) }
|
||||||
.onSuccess { _uiState.update { it.copy(isSaving = false, phase = MemoryLanePhase.LIST) } }
|
.onSuccess { _uiState.update { it.copy(isSaving = false, phase = MemoryLanePhase.LIST) } }
|
||||||
.onFailure {
|
.onFailure { e ->
|
||||||
Log.w(TAG, "Could not save capsule", it)
|
Log.w(TAG, "Could not save capsule", e)
|
||||||
_uiState.update { s -> s.copy(isSaving = false, error = "Could not save. Try again.") }
|
val msg = if (e is IllegalStateException) e.message ?: "Encryption not ready. Try again." else "Could not save. Check your connection."
|
||||||
|
_uiState.update { s -> s.copy(isSaving = false, error = msg) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,11 @@ class ThisOrThatViewModel @Inject constructor(
|
||||||
val sId = sessionId ?: return
|
val sId = sessionId ?: return
|
||||||
val uid = userId ?: return
|
val uid = userId ?: return
|
||||||
runCatching { dataSource.submitAnswers(cId, sId, uid, answers) }
|
runCatching { dataSource.submitAnswers(cId, sId, uid, answers) }
|
||||||
.onFailure { Log.w(TAG, "Could not submit answers", it) }
|
.onFailure { e ->
|
||||||
|
submitted = false
|
||||||
|
Log.w(TAG, "Could not submit answers", e)
|
||||||
|
_uiState.update { it.copy(phase = TotPhase.ERROR, error = "Couldn't save your answers. Check your connection and go back to try again.") }
|
||||||
|
}
|
||||||
// The observer flips WAITING → REVEAL once the partner's answers land
|
// The observer flips WAITING → REVEAL once the partner's answers land
|
||||||
// (or right away, if they finished first).
|
// (or right away, if they finished first).
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import app.closer.core.navigation.AppRoute
|
import app.closer.core.navigation.AppRoute
|
||||||
import app.closer.domain.model.GameType
|
import app.closer.domain.model.GameType
|
||||||
import app.closer.domain.model.QuestionSession
|
import app.closer.domain.model.QuestionSession
|
||||||
|
import app.closer.domain.model.TimeCapsule
|
||||||
import app.closer.ui.components.EmptyState
|
import app.closer.ui.components.EmptyState
|
||||||
import app.closer.ui.components.ErrorState
|
import app.closer.ui.components.ErrorState
|
||||||
import app.closer.ui.components.LoadingState
|
import app.closer.ui.components.LoadingState
|
||||||
|
|
@ -100,30 +101,49 @@ fun GameHistoryScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val hasAny = state.sessions.isNotEmpty() || state.completedChallenges.isNotEmpty() || state.unlockedCapsules.isNotEmpty()
|
||||||
|
|
||||||
when {
|
when {
|
||||||
!state.hasPremium -> item {
|
!state.hasPremium -> item {
|
||||||
GameHistoryLockedCard(onUnlock = { onNavigate(AppRoute.PAYWALL) })
|
GameHistoryLockedCard(onUnlock = { onNavigate(AppRoute.PAYWALL) })
|
||||||
}
|
}
|
||||||
state.isLoading -> item { LoadingState(message = "Loading your sessions…") }
|
state.isLoading -> item { LoadingState(message = "Loading your games…") }
|
||||||
state.error != null -> item {
|
state.error != null -> item {
|
||||||
ErrorState(
|
ErrorState(
|
||||||
message = state.error!!,
|
message = state.error!!,
|
||||||
onRetry = viewModel::load
|
onRetry = viewModel::load
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
state.sessions.isEmpty() -> item {
|
!hasAny -> item {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "No games played yet",
|
title = "No games played yet",
|
||||||
body = "Finish a round of This or That, How Well, Desire Sync, or Spin the Wheel — your results will show up here so you can revisit them together.",
|
body = "Finish a round of This or That, How Well, Desire Sync, Spin the Wheel, a Connection Challenge, or Memory Lane — your results will show up here.",
|
||||||
actionLabel = "Play a game",
|
actionLabel = "Play a game",
|
||||||
onAction = { onNavigate(AppRoute.PLAY) }
|
onAction = { onNavigate(AppRoute.PLAY) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> items(state.sessions, key = { it.id }) { session ->
|
else -> {
|
||||||
WheelSessionCard(
|
if (state.sessions.isNotEmpty()) {
|
||||||
session = session,
|
item { HistorySectionHeader("Game Sessions") }
|
||||||
onClick = { onNavigate(sessionReplayRoute(session)) }
|
items(state.sessions, key = { it.id }) { session ->
|
||||||
)
|
WheelSessionCard(
|
||||||
|
session = session,
|
||||||
|
onClick = { onNavigate(sessionReplayRoute(session)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state.completedChallenges.isNotEmpty()) {
|
||||||
|
item { HistorySectionHeader("Completed Challenges") }
|
||||||
|
items(state.completedChallenges, key = { it.challengeId }) { entry ->
|
||||||
|
ChallengeHistoryCard(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state.unlockedCapsules.isNotEmpty()) {
|
||||||
|
item { HistorySectionHeader("Memory Lane") }
|
||||||
|
items(state.unlockedCapsules, key = { it.id }) { capsule ->
|
||||||
|
CapsuleHistoryCard(capsule)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -169,6 +189,95 @@ private fun WheelSessionCard(session: QuestionSession, onClick: () -> Unit = {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HistorySectionHeader(title: String) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(top = 8.dp, bottom = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ChallengeHistoryCard(entry: CompletedChallengeEntry) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = closerCardColor(alpha = 0.88f)),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(entry.emoji, style = MaterialTheme.typography.headlineSmall)
|
||||||
|
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
|
Text(
|
||||||
|
text = entry.title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Challenge complete",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (entry.startedAt > 0L) {
|
||||||
|
Text(
|
||||||
|
text = SimpleDateFormat("d MMM", java.util.Locale.getDefault()).format(Date(entry.startedAt)),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = Color(0xFFB98AF4)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CapsuleHistoryCard(capsule: TimeCapsule) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = closerCardColor(alpha = 0.88f)),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text("💌", style = MaterialTheme.typography.headlineSmall)
|
||||||
|
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
|
Text(
|
||||||
|
text = capsule.title.ifBlank { "Untitled capsule" },
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Memory Lane",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (capsule.unlockAt > 0L) {
|
||||||
|
Text(
|
||||||
|
text = SimpleDateFormat("d MMM", java.util.Locale.getDefault()).format(Date(capsule.unlockAt)),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = Color(0xFFB98AF4)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun sessionTitle(session: QuestionSession): String = when (session.gameType) {
|
private fun sessionTitle(session: QuestionSession): String = when (session.gameType) {
|
||||||
GameType.THIS_OR_THAT -> "This or That"
|
GameType.THIS_OR_THAT -> "This or That"
|
||||||
GameType.HOW_WELL -> "How Well Do You Know Me"
|
GameType.HOW_WELL -> "How Well Do You Know Me"
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,16 @@ package app.closer.ui.wheel
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.closer.core.billing.EntitlementChecker
|
import app.closer.core.billing.EntitlementChecker
|
||||||
|
import app.closer.data.challenges.ChallengesCatalog
|
||||||
|
import app.closer.data.remote.FirestoreCapsuleDataSource
|
||||||
|
import app.closer.data.remote.FirestoreChallengeDataSource
|
||||||
import app.closer.domain.model.QuestionSession
|
import app.closer.domain.model.QuestionSession
|
||||||
|
import app.closer.domain.model.TimeCapsule
|
||||||
import app.closer.domain.repository.AuthRepository
|
import app.closer.domain.repository.AuthRepository
|
||||||
import app.closer.domain.repository.CoupleRepository
|
import app.closer.domain.repository.CoupleRepository
|
||||||
import app.closer.domain.repository.QuestionSessionRepository
|
import app.closer.domain.repository.QuestionSessionRepository
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
@ -17,9 +22,18 @@ import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
data class CompletedChallengeEntry(
|
||||||
|
val challengeId: String,
|
||||||
|
val title: String,
|
||||||
|
val emoji: String,
|
||||||
|
val startedAt: Long
|
||||||
|
)
|
||||||
|
|
||||||
data class GameHistoryUiState(
|
data class GameHistoryUiState(
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val sessions: List<QuestionSession> = emptyList(),
|
val sessions: List<QuestionSession> = emptyList(),
|
||||||
|
val completedChallenges: List<CompletedChallengeEntry> = emptyList(),
|
||||||
|
val unlockedCapsules: List<TimeCapsule> = emptyList(),
|
||||||
val hasPremium: Boolean = false,
|
val hasPremium: Boolean = false,
|
||||||
val error: String? = null
|
val error: String? = null
|
||||||
)
|
)
|
||||||
|
|
@ -29,6 +43,8 @@ class GameHistoryViewModel @Inject constructor(
|
||||||
private val sessionRepository: QuestionSessionRepository,
|
private val sessionRepository: QuestionSessionRepository,
|
||||||
private val authRepository: AuthRepository,
|
private val authRepository: AuthRepository,
|
||||||
private val coupleRepository: CoupleRepository,
|
private val coupleRepository: CoupleRepository,
|
||||||
|
private val challengeDataSource: FirestoreChallengeDataSource,
|
||||||
|
private val capsuleDataSource: FirestoreCapsuleDataSource,
|
||||||
entitlementChecker: EntitlementChecker
|
entitlementChecker: EntitlementChecker
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
|
@ -53,12 +69,48 @@ class GameHistoryViewModel @Inject constructor(
|
||||||
_uiState.update { it.copy(isLoading = false) }
|
_uiState.update { it.copy(isLoading = false) }
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
sessionRepository.getSessionsForCouple(couple.id)
|
val coupleId = couple.id
|
||||||
|
|
||||||
|
val sessionsDeferred = async { sessionRepository.getSessionsForCouple(coupleId) }
|
||||||
|
val challengesDeferred = async { runCatching { challengeDataSource.getCompletedChallengeIds(coupleId) }.getOrElse { emptyList() } }
|
||||||
|
val capsulesDeferred = async { runCatching { capsuleDataSource.getCapsules(coupleId) }.getOrElse { emptyList() } }
|
||||||
|
|
||||||
|
val sessionsResult = sessionsDeferred.await()
|
||||||
|
val completedChallengeIds = challengesDeferred.await()
|
||||||
|
val allCapsules = capsulesDeferred.await()
|
||||||
|
|
||||||
|
val completedChallenges = completedChallengeIds.mapNotNull { (challengeId, startedAt) ->
|
||||||
|
val def = ChallengesCatalog.findById(challengeId) ?: return@mapNotNull null
|
||||||
|
CompletedChallengeEntry(
|
||||||
|
challengeId = challengeId,
|
||||||
|
title = def.title,
|
||||||
|
emoji = def.emoji,
|
||||||
|
startedAt = startedAt
|
||||||
|
)
|
||||||
|
}.sortedByDescending { it.startedAt }
|
||||||
|
|
||||||
|
val unlockedCapsules = allCapsules.filter { it.status == "unlocked" }
|
||||||
|
|
||||||
|
sessionsResult
|
||||||
.onSuccess { sessions ->
|
.onSuccess { sessions ->
|
||||||
_uiState.update { it.copy(isLoading = false, sessions = sessions) }
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
sessions = sessions,
|
||||||
|
completedChallenges = completedChallenges,
|
||||||
|
unlockedCapsules = unlockedCapsules
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onFailure { e ->
|
.onFailure { e ->
|
||||||
_uiState.update { it.copy(isLoading = false, error = e.message ?: "Couldn't load your past games.") }
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
completedChallenges = completedChallenges,
|
||||||
|
unlockedCapsules = unlockedCapsules,
|
||||||
|
error = e.message ?: "Couldn't load your past games."
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue