diff --git a/app/src/main/java/app/closer/core/navigation/AppNavigation.kt b/app/src/main/java/app/closer/core/navigation/AppNavigation.kt index d48653fa..095c37e0 100644 --- a/app/src/main/java/app/closer/core/navigation/AppNavigation.kt +++ b/app/src/main/java/app/closer/core/navigation/AppNavigation.kt @@ -46,8 +46,6 @@ import app.closer.ui.pairing.RecoveryScreen import app.closer.ui.dates.DateMatchScreen import app.closer.ui.dates.DateMatchesScreen import app.closer.ui.dates.DateBuilderScreen -import app.closer.ui.dates.DateMemoriesScreen -import app.closer.ui.dates.DateReflectionScreen import app.closer.ui.dates.BucketListScreen import app.closer.ui.paywall.PaywallScreen import app.closer.ui.play.PlayHubScreen @@ -511,15 +509,6 @@ fun AppNavigation( composable(route = AppRoute.DATE_BUILDER) { DateBuilderScreen(onNavigate = navigateRoute) } - composable(route = AppRoute.DATE_MEMORIES) { - DateMemoriesScreen(onNavigate = navigateRoute) - } - composable( - route = AppRoute.DATE_REFLECTION, - arguments = listOf(navArgument("dateId") { type = NavType.StringType }) - ) { - DateReflectionScreen(onNavigate = navigateRoute) - } composable(route = AppRoute.BUCKET_LIST) { BucketListScreen(onNavigate = navigateRoute) } diff --git a/app/src/main/java/app/closer/core/navigation/AppRoute.kt b/app/src/main/java/app/closer/core/navigation/AppRoute.kt index 49e061bd..f46c0ad9 100644 --- a/app/src/main/java/app/closer/core/navigation/AppRoute.kt +++ b/app/src/main/java/app/closer/core/navigation/AppRoute.kt @@ -46,8 +46,6 @@ object AppRoute { const val DATE_MATCH = "date_match" const val DATE_MATCHES = "date_matches" const val DATE_BUILDER = "date_builder" - const val DATE_MEMORIES = "date_memories" - const val DATE_REFLECTION = "date_reflection/{dateId}" const val BUCKET_LIST = "bucket_list" const val THIS_OR_THAT = "this_or_that" const val HOW_WELL = "how_well" @@ -197,8 +195,6 @@ object AppRoute { fun answerReveal(questionId: String): String = "answer_reveal/${questionId.asRouteArg()}" - fun dateReflection(dateId: String): String = "date_reflection/${dateId.asRouteArg()}" - fun conversation(coupleId: String, conversationId: String): String = "conversation/${coupleId.asRouteArg()}/${conversationId.asRouteArg()}" diff --git a/app/src/main/java/app/closer/ui/dates/DateMatchesScreen.kt b/app/src/main/java/app/closer/ui/dates/DateMatchesScreen.kt index f52437e0..a99c9a99 100644 --- a/app/src/main/java/app/closer/ui/dates/DateMatchesScreen.kt +++ b/app/src/main/java/app/closer/ui/dates/DateMatchesScreen.kt @@ -29,9 +29,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.foundation.clickable -import androidx.compose.material3.TextButton -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -45,13 +42,8 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import app.closer.domain.model.DateCostLevel import app.closer.domain.model.DateIdea -import app.closer.core.navigation.AppRoute import app.closer.domain.model.DateMatch import app.closer.domain.model.DateMatchSuggestion -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.width -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource import app.closer.domain.model.SwipeAction import app.closer.ui.components.EmptyState import app.closer.ui.components.ErrorState @@ -65,17 +57,10 @@ fun DateMatchesScreen( ) { val state by viewModel.uiState.collectAsState() - // After marking a date done, open its reflection. - LaunchedEffect(Unit) { - viewModel.markedDateId.collect { dateId -> onNavigate(AppRoute.dateReflection(dateId)) } - } - DateMatchesContent( state = state, onRetry = viewModel::retry, - onBack = { onNavigate("back") }, - onMarkCompleted = viewModel::markCompleted, - onMemories = { onNavigate(AppRoute.DATE_MEMORIES) } + onBack = { onNavigate("back") } ) } @@ -83,9 +68,7 @@ fun DateMatchesScreen( private fun DateMatchesContent( state: DateMatchesUiState, onRetry: () -> Unit, - onBack: () -> Unit, - onMarkCompleted: (DateMatch) -> Unit = {}, - onMemories: () -> Unit = {} + onBack: () -> Unit ) { Box( modifier = Modifier @@ -121,16 +104,6 @@ private fun DateMatchesContent( style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) - TextButton(onClick = onMemories, contentPadding = androidx.compose.foundation.layout.PaddingValues(vertical = 4.dp)) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.glyph_date_replay), - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.primary - ) - Spacer(Modifier.width(6.dp)) - Text("Your date memories", style = MaterialTheme.typography.labelLarge) - } } } @@ -176,7 +149,7 @@ private fun DateMatchesContent( ) } items(state.mutualMatches, key = { it.id }) { match -> - MatchCard(match = match, onMarkCompleted = { onMarkCompleted(match) }) + MatchCard(match = match) } } @@ -246,22 +219,10 @@ private fun SectionHeader( } @Composable -private fun MatchCard(match: DateMatch, onMarkCompleted: () -> Unit = {}) { +private fun MatchCard(match: DateMatch) { val idea = match.dateIdea ?: return IdeaCard( idea = idea, - action = { - TextButton(onClick = onMarkCompleted, modifier = Modifier.fillMaxWidth()) { - Icon( - imageVector = CloserGlyphs.Heart, - contentDescription = null, - modifier = Modifier.size(16.dp), - tint = Color(0xFF9B1B5A) - ) - Spacer(Modifier.width(6.dp)) - Text("We did this", fontWeight = FontWeight.SemiBold) - } - }, badge = { Surface( shape = RoundedCornerShape(999.dp), @@ -325,8 +286,7 @@ private fun SuggestionCard(suggestion: DateMatchSuggestion) { @Composable private fun IdeaCard( idea: DateIdea, - badge: @Composable () -> Unit, - action: (@Composable () -> Unit)? = null + badge: @Composable () -> Unit ) { Card( modifier = Modifier.fillMaxWidth(), @@ -388,8 +348,6 @@ private fun IdeaCard( InfoChip(label = "Premium", emphasis = true) } } - - action?.invoke() } } } diff --git a/app/src/main/java/app/closer/ui/dates/DateMatchesViewModel.kt b/app/src/main/java/app/closer/ui/dates/DateMatchesViewModel.kt index 4c47b896..90996771 100644 --- a/app/src/main/java/app/closer/ui/dates/DateMatchesViewModel.kt +++ b/app/src/main/java/app/closer/ui/dates/DateMatchesViewModel.kt @@ -3,11 +3,9 @@ package app.closer.ui.dates import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.closer.data.remote.FirestoreDateMemoryDataSource import app.closer.data.repository.DateIdeaSeed import app.closer.domain.model.DateIdea import app.closer.domain.model.DateMatch -import app.closer.domain.model.DateMemory import app.closer.domain.model.DateMatchSuggestion import app.closer.domain.model.DateSwipe import app.closer.domain.model.SwipeAction @@ -18,11 +16,8 @@ import app.closer.domain.repository.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @@ -47,44 +42,16 @@ class DateMatchesViewModel @Inject constructor( private val repository: DateMatchRepository, private val authRepository: AuthRepository, private val coupleRepository: CoupleRepository, - private val userRepository: UserRepository, - private val memoryDataSource: FirestoreDateMemoryDataSource + private val userRepository: UserRepository ) : ViewModel() { private val _uiState = MutableStateFlow(DateMatchesUiState()) val uiState: StateFlow = _uiState.asStateFlow() - /** Emits the dateId to open its reflection screen right after a date is marked done. */ - private val _markedDateId = MutableSharedFlow(extraBufferCapacity = 1) - val markedDateId: SharedFlow = _markedDateId.asSharedFlow() - init { loadMatches() } - /** Log a matched date as completed (idempotent) → then open its reflection. */ - fun markCompleted(match: DateMatch) { - val cid = _uiState.value.coupleId ?: return - val uid = authRepository.currentUserId ?: return - val idea = match.dateIdea ?: DateIdeaSeed.byId(match.dateIdeaId) - viewModelScope.launch { - runCatching { - memoryDataSource.markCompleted( - cid, - DateMemory( - id = match.id, - dateIdeaId = match.dateIdeaId, - title = idea?.title ?: "", - category = idea?.category ?: "", - completedAt = System.currentTimeMillis(), - addedBy = uid - ) - ) - }.onSuccess { _markedDateId.tryEmit(match.id) } - .onFailure { Log.w(TAG, "markCompleted failed", it) } - } - } - private fun loadMatches() { viewModelScope.launch { _uiState.value = DateMatchesUiState(isLoading = true) diff --git a/app/src/main/java/app/closer/ui/dates/DateMemoriesScreen.kt b/app/src/main/java/app/closer/ui/dates/DateMemoriesScreen.kt deleted file mode 100644 index 6d8503ba..00000000 --- a/app/src/main/java/app/closer/ui/dates/DateMemoriesScreen.kt +++ /dev/null @@ -1,197 +0,0 @@ -package app.closer.ui.dates - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import app.closer.R -import app.closer.core.navigation.AppRoute -import app.closer.data.remote.FirestoreDateMemoryDataSource -import app.closer.data.remote.FirestoreDateReflectionDataSource -import app.closer.domain.model.DateMemory -import app.closer.domain.model.DateReflectionState -import app.closer.domain.repository.AuthRepository -import app.closer.domain.repository.CoupleRepository -import app.closer.ui.components.BrandIllustration -import app.closer.ui.components.CloserCard -import app.closer.ui.components.CloserHeartLoader -import app.closer.ui.settings.SettingsSubpage -import app.closer.ui.theme.CloserPalette -import app.closer.ui.theme.closerCardColor -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import java.text.DateFormat -import java.util.Date -import javax.inject.Inject - -data class DateMemoryRow(val memory: DateMemory, val state: DateReflectionState) - -data class DateMemoriesUiState( - val isLoading: Boolean = true, - val rows: List = emptyList() -) - -@HiltViewModel -class DateMemoriesViewModel @Inject constructor( - private val memoryDataSource: FirestoreDateMemoryDataSource, - private val reflectionDataSource: FirestoreDateReflectionDataSource, - private val authRepository: AuthRepository, - private val coupleRepository: CoupleRepository -) : ViewModel() { - - private val _uiState = MutableStateFlow(DateMemoriesUiState()) - val uiState: StateFlow = _uiState.asStateFlow() - - init { - viewModelScope.launch { - val uid = authRepository.currentUserId - val couple = uid?.let { runCatching { coupleRepository.getCoupleForUser(it) }.getOrNull() } - if (uid == null || couple == null) { - _uiState.update { it.copy(isLoading = false) } - return@launch - } - val partnerId = couple.userIds.firstOrNull { it != uid } - memoryDataSource.observeHistory(couple.id).collect { memories -> - val rows = memories.map { m -> - val mine = runCatching { reflectionDataSource.hasReflected(couple.id, m.id, uid) }.getOrDefault(false) - val partner = partnerId?.let { - runCatching { reflectionDataSource.hasReflected(couple.id, m.id, it) }.getOrDefault(false) - } ?: false - val state = when { - mine && partner -> DateReflectionState.BOTH_DONE - mine -> DateReflectionState.AWAITING_PARTNER - else -> DateReflectionState.NONE - } - DateMemoryRow(m, state) - } - _uiState.update { it.copy(isLoading = false, rows = rows) } - } - } - } -} - -@Composable -fun DateMemoriesScreen( - onNavigate: (String) -> Unit = {}, - viewModel: DateMemoriesViewModel = hiltViewModel() -) { - val state by viewModel.uiState.collectAsState() - SettingsSubpage(title = "Date memories", onBack = { onNavigate("back") }) { padding -> - when { - state.isLoading -> Box(Modifier.fillMaxSize().padding(padding), Alignment.Center) { CloserHeartLoader() } - state.rows.isEmpty() -> DateMemoriesEmpty(Modifier.padding(padding)) - else -> LazyColumn( - modifier = Modifier.fillMaxSize().padding(padding).padding(horizontal = 20.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - items(state.rows, key = { it.memory.id }) { row -> - DateMemoryCard(row) { onNavigate(AppRoute.dateReflection(row.memory.id)) } - } - } - } - } -} - -@Composable -private fun DateMemoryCard(row: DateMemoryRow, onClick: () -> Unit) { - CloserCard(modifier = Modifier.fillMaxWidth().clickable(onClick = onClick), containerColor = closerCardColor()) { - Row( - modifier = Modifier.fillMaxWidth().padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text( - text = row.memory.title.ifBlank { "Your date" }, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface - ) - val meta = listOfNotNull( - row.memory.category.takeIf { it.isNotBlank() }?.replace('_', ' '), - row.memory.completedAt.takeIf { it > 0L } - ?.let { DateFormat.getDateInstance(DateFormat.MEDIUM).format(Date(it)) } - ).joinToString(" · ") - if (meta.isNotBlank()) { - Text(meta, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) - } - } - ReflectionChip(row.state) - } - } -} - -@Composable -private fun ReflectionChip(state: DateReflectionState) { - val (label, color) = when (state) { - DateReflectionState.NONE -> "Reflect" to MaterialTheme.colorScheme.primary - DateReflectionState.AWAITING_PARTNER -> "Waiting" to CloserPalette.Gold - DateReflectionState.BOTH_DONE -> "View" to CloserPalette.Evergreen - } - Surface(shape = RoundedCornerShape(50), color = color.copy(alpha = 0.14f)) { - Text( - text = label, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.Medium, - color = color - ) - } -} - -@Composable -private fun DateMemoriesEmpty(modifier: Modifier = Modifier) { - Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Column( - modifier = Modifier.padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - BrandIllustration( - res = R.drawable.illustration_date_memories_empty, - contentDescription = null, - modifier = Modifier.size(200.dp) - ) - Text( - text = "Your dates, remembered", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center, - modifier = Modifier.padding(top = 16.dp) - ) - Text( - text = "When you go on a date, mark it done and reflect together — your moments will gather here.", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - modifier = Modifier.padding(top = 6.dp) - ) - } - } -} diff --git a/app/src/main/java/app/closer/ui/dates/DateReflectionScreen.kt b/app/src/main/java/app/closer/ui/dates/DateReflectionScreen.kt deleted file mode 100644 index aaf82533..00000000 --- a/app/src/main/java/app/closer/ui/dates/DateReflectionScreen.kt +++ /dev/null @@ -1,286 +0,0 @@ -package app.closer.ui.dates - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import app.closer.core.crash.CrashReporter -import app.closer.data.remote.FirestoreDateReflectionDataSource -import app.closer.domain.model.DateReflection -import app.closer.domain.repository.AuthRepository -import app.closer.domain.repository.CoupleRepository -import app.closer.domain.repository.UserRepository -import app.closer.ui.components.CloserCard -import app.closer.ui.components.CloserHeartLoader -import app.closer.ui.settings.SettingsSubpage -import app.closer.ui.theme.closerCardColor -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import javax.inject.Inject - -/** The three fixed post-date reflection prompts. */ -private val REFLECTION_PROMPTS = listOf( - "Your favorite moment", - "What surprised you", - "What you appreciated most" -) - -enum class ReflectionPhase { LOADING, EDIT, AWAITING_PARTNER, REVEALED, ERROR } - -data class DateReflectionUiState( - val phase: ReflectionPhase = ReflectionPhase.LOADING, - val partnerName: String? = null, - val favoriteMoment: String = "", - val surprise: String = "", - val appreciated: String = "", - val mine: DateReflection? = null, - val partner: DateReflection? = null, - val isSaving: Boolean = false, - val error: String? = null -) - -@HiltViewModel -class DateReflectionViewModel @Inject constructor( - private val reflectionDataSource: FirestoreDateReflectionDataSource, - private val authRepository: AuthRepository, - private val coupleRepository: CoupleRepository, - private val userRepository: UserRepository, - private val crashReporter: CrashReporter, - savedStateHandle: SavedStateHandle -) : ViewModel() { - - private val dateId: String = savedStateHandle["dateId"] ?: "" - private val _uiState = MutableStateFlow(DateReflectionUiState()) - val uiState: StateFlow = _uiState.asStateFlow() - - private var coupleId: String? = null - private var partnerId: String? = null - - init { load() } - - private fun load() { - viewModelScope.launch { - val uid = authRepository.currentUserId - val couple = uid?.let { runCatching { coupleRepository.getCoupleForUser(it) }.getOrNull() } - if (uid == null || couple == null) { - _uiState.update { it.copy(phase = ReflectionPhase.ERROR, error = "Not paired.") } - return@launch - } - coupleId = couple.id - partnerId = couple.userIds.firstOrNull { it != uid } - val partnerName = partnerId?.let { - runCatching { userRepository.getUser(it)?.displayName }.getOrNull() - }?.takeIf { it.isNotBlank() } ?: "your partner" - _uiState.update { it.copy(partnerName = partnerName) } - refresh() - // Live-complete the reveal the moment the partner reflects. - partnerId?.let { pid -> - launch { - reflectionDataSource.observeReflected(couple.id, dateId, pid).collect { partnerReflected -> - if (partnerReflected && _uiState.value.phase == ReflectionPhase.AWAITING_PARTNER) refresh() - } - } - } - } - } - - private suspend fun refresh() { - val cid = coupleId ?: return - val uid = authRepository.currentUserId ?: return - val pid = partnerId - val iReflected = runCatching { reflectionDataSource.hasReflected(cid, dateId, uid) }.getOrDefault(false) - val partnerReflected = pid?.let { - runCatching { reflectionDataSource.hasReflected(cid, dateId, it) }.getOrDefault(false) - } ?: false - when { - !iReflected -> _uiState.update { it.copy(phase = ReflectionPhase.EDIT) } - iReflected && !partnerReflected -> { - val mine = runCatching { reflectionDataSource.decryptReflectionFor(cid, dateId, uid) }.getOrNull() - _uiState.update { it.copy(phase = ReflectionPhase.AWAITING_PARTNER, mine = mine) } - } - else -> { - val mine = runCatching { reflectionDataSource.decryptReflectionFor(cid, dateId, uid) }.getOrNull() - val partner = pid?.let { - runCatching { reflectionDataSource.decryptReflectionFor(cid, dateId, it) }.getOrNull() - } - runCatching { reflectionDataSource.markRevealed(cid, dateId, uid) } - _uiState.update { it.copy(phase = ReflectionPhase.REVEALED, mine = mine, partner = partner) } - } - } - } - - fun onFavoriteMoment(v: String) = _uiState.update { it.copy(favoriteMoment = v) } - fun onSurprise(v: String) = _uiState.update { it.copy(surprise = v) } - fun onAppreciated(v: String) = _uiState.update { it.copy(appreciated = v) } - - fun save() { - val state = _uiState.value - if (state.isSaving) return - val cid = coupleId - val uid = authRepository.currentUserId - if (cid == null || uid == null) return - if (state.favoriteMoment.isBlank() && state.surprise.isBlank() && state.appreciated.isBlank()) { - _uiState.update { it.copy(error = "Add at least one reflection first.") } - return - } - _uiState.update { it.copy(isSaving = true, error = null) } - viewModelScope.launch { - runCatching { - reflectionDataSource.saveReflection( - cid, dateId, uid, - DateReflection( - dateId = dateId, userId = uid, - favoriteMoment = state.favoriteMoment.trim(), - surprise = state.surprise.trim(), - appreciated = state.appreciated.trim() - ) - ) - }.onFailure { - crashReporter.recordException(it) - _uiState.update { s -> s.copy(isSaving = false, error = "Couldn't save. Try again.") } - return@launch - } - _uiState.update { it.copy(isSaving = false) } - refresh() - } - } - - fun dismissError() = _uiState.update { it.copy(error = null) } -} - -@Composable -fun DateReflectionScreen( - onNavigate: (String) -> Unit = {}, - viewModel: DateReflectionViewModel = hiltViewModel() -) { - val state by viewModel.uiState.collectAsState() - SettingsSubpage(title = "Date reflection", onBack = { onNavigate("back") }) { padding -> - when (state.phase) { - ReflectionPhase.LOADING -> Box(Modifier.fillMaxSize().padding(padding), Alignment.Center) { CloserHeartLoader() } - ReflectionPhase.ERROR -> Box(Modifier.fillMaxSize().padding(padding), Alignment.Center) { - Text(state.error ?: "Something went wrong.", color = MaterialTheme.colorScheme.onSurfaceVariant) - } - else -> Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .verticalScroll(rememberScrollState()) - .padding(horizontal = 20.dp, vertical = 12.dp), - verticalArrangement = Arrangement.spacedBy(14.dp) - ) { - when (state.phase) { - ReflectionPhase.EDIT -> ReflectionEditor(state, viewModel) - ReflectionPhase.AWAITING_PARTNER -> AwaitingPartner(state) - ReflectionPhase.REVEALED -> RevealedReflections(state) - else -> {} - } - Spacer(Modifier.height(12.dp)) - } - } - } -} - -@Composable -private fun ReflectionEditor(state: DateReflectionUiState, viewModel: DateReflectionViewModel) { - Text( - "Reflect privately — your words stay sealed until you've both shared.", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - OutlinedTextField( - value = state.favoriteMoment, onValueChange = viewModel::onFavoriteMoment, - label = { Text(REFLECTION_PROMPTS[0]) }, modifier = Modifier.fillMaxWidth(), minLines = 2 - ) - OutlinedTextField( - value = state.surprise, onValueChange = viewModel::onSurprise, - label = { Text(REFLECTION_PROMPTS[1]) }, modifier = Modifier.fillMaxWidth(), minLines = 2 - ) - OutlinedTextField( - value = state.appreciated, onValueChange = viewModel::onAppreciated, - label = { Text(REFLECTION_PROMPTS[2]) }, modifier = Modifier.fillMaxWidth(), minLines = 2 - ) - state.error?.let { Text(it, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall) } - Button(onClick = viewModel::save, enabled = !state.isSaving, modifier = Modifier.fillMaxWidth()) { - Text(if (state.isSaving) "Saving…" else "Save my reflection") - } -} - -@Composable -private fun AwaitingPartner(state: DateReflectionUiState) { - Text( - "Saved privately 💜 — waiting for ${state.partnerName} to reflect. You'll reveal together.", - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - state.mine?.let { ReflectionCard(title = "Your reflection", reflection = it) } -} - -@Composable -private fun RevealedReflections(state: DateReflectionUiState) { - Text( - "You both reflected 💜", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface - ) - REFLECTION_PROMPTS.forEachIndexed { i, prompt -> - CloserCard(modifier = Modifier.fillMaxWidth(), containerColor = closerCardColor()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text(prompt, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary) - Text("You", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) - Text(promptValue(state.mine, i).ifBlank { "—" }, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface) - Text(state.partnerName ?: "Partner", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) - Text(promptValue(state.partner, i).ifBlank { "—" }, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface) - } - } - } -} - -@Composable -private fun ReflectionCard(title: String, reflection: DateReflection) { - CloserCard(modifier = Modifier.fillMaxWidth(), containerColor = closerCardColor()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text(title, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary) - REFLECTION_PROMPTS.forEachIndexed { i, prompt -> - val v = promptValue(reflection, i) - if (v.isNotBlank()) { - Text(prompt, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) - Text(v, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface) - } - } - } - } -} - -private fun promptValue(r: DateReflection?, index: Int): String = when (index) { - 0 -> r?.favoriteMoment.orEmpty() - 1 -> r?.surprise.orEmpty() - else -> r?.appreciated.orEmpty() -}