Revert "feat(date-memories): add DateMemories timeline + DateReflection screen, wire into nav (batch 2/8)"

This reverts commit 15087df13b.
This commit is contained in:
null 2026-06-30 18:13:00 -05:00
parent 5905c2b2d0
commit 6a0849deb7
6 changed files with 6 additions and 579 deletions

View File

@ -46,8 +46,6 @@ import app.closer.ui.pairing.RecoveryScreen
import app.closer.ui.dates.DateMatchScreen import app.closer.ui.dates.DateMatchScreen
import app.closer.ui.dates.DateMatchesScreen import app.closer.ui.dates.DateMatchesScreen
import app.closer.ui.dates.DateBuilderScreen 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.dates.BucketListScreen
import app.closer.ui.paywall.PaywallScreen import app.closer.ui.paywall.PaywallScreen
import app.closer.ui.play.PlayHubScreen import app.closer.ui.play.PlayHubScreen
@ -511,15 +509,6 @@ fun AppNavigation(
composable(route = AppRoute.DATE_BUILDER) { composable(route = AppRoute.DATE_BUILDER) {
DateBuilderScreen(onNavigate = navigateRoute) 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) { composable(route = AppRoute.BUCKET_LIST) {
BucketListScreen(onNavigate = navigateRoute) BucketListScreen(onNavigate = navigateRoute)
} }

View File

@ -46,8 +46,6 @@ object AppRoute {
const val DATE_MATCH = "date_match" const val DATE_MATCH = "date_match"
const val DATE_MATCHES = "date_matches" const val DATE_MATCHES = "date_matches"
const val DATE_BUILDER = "date_builder" 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 BUCKET_LIST = "bucket_list"
const val THIS_OR_THAT = "this_or_that" const val THIS_OR_THAT = "this_or_that"
const val HOW_WELL = "how_well" const val HOW_WELL = "how_well"
@ -197,8 +195,6 @@ object AppRoute {
fun answerReveal(questionId: String): String = "answer_reveal/${questionId.asRouteArg()}" 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 = fun conversation(coupleId: String, conversationId: String): String =
"conversation/${coupleId.asRouteArg()}/${conversationId.asRouteArg()}" "conversation/${coupleId.asRouteArg()}/${conversationId.asRouteArg()}"

View File

@ -29,9 +29,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -45,13 +42,8 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import app.closer.domain.model.DateCostLevel import app.closer.domain.model.DateCostLevel
import app.closer.domain.model.DateIdea import app.closer.domain.model.DateIdea
import app.closer.core.navigation.AppRoute
import app.closer.domain.model.DateMatch import app.closer.domain.model.DateMatch
import app.closer.domain.model.DateMatchSuggestion 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.domain.model.SwipeAction
import app.closer.ui.components.EmptyState import app.closer.ui.components.EmptyState
import app.closer.ui.components.ErrorState import app.closer.ui.components.ErrorState
@ -65,17 +57,10 @@ fun DateMatchesScreen(
) { ) {
val state by viewModel.uiState.collectAsState() 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( DateMatchesContent(
state = state, state = state,
onRetry = viewModel::retry, onRetry = viewModel::retry,
onBack = { onNavigate("back") }, onBack = { onNavigate("back") }
onMarkCompleted = viewModel::markCompleted,
onMemories = { onNavigate(AppRoute.DATE_MEMORIES) }
) )
} }
@ -83,9 +68,7 @@ fun DateMatchesScreen(
private fun DateMatchesContent( private fun DateMatchesContent(
state: DateMatchesUiState, state: DateMatchesUiState,
onRetry: () -> Unit, onRetry: () -> Unit,
onBack: () -> Unit, onBack: () -> Unit
onMarkCompleted: (DateMatch) -> Unit = {},
onMemories: () -> Unit = {}
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
@ -121,16 +104,6 @@ private fun DateMatchesContent(
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant 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 -> items(state.mutualMatches, key = { it.id }) { match ->
MatchCard(match = match, onMarkCompleted = { onMarkCompleted(match) }) MatchCard(match = match)
} }
} }
@ -246,22 +219,10 @@ private fun SectionHeader(
} }
@Composable @Composable
private fun MatchCard(match: DateMatch, onMarkCompleted: () -> Unit = {}) { private fun MatchCard(match: DateMatch) {
val idea = match.dateIdea ?: return val idea = match.dateIdea ?: return
IdeaCard( IdeaCard(
idea = idea, 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 = { badge = {
Surface( Surface(
shape = RoundedCornerShape(999.dp), shape = RoundedCornerShape(999.dp),
@ -325,8 +286,7 @@ private fun SuggestionCard(suggestion: DateMatchSuggestion) {
@Composable @Composable
private fun IdeaCard( private fun IdeaCard(
idea: DateIdea, idea: DateIdea,
badge: @Composable () -> Unit, badge: @Composable () -> Unit
action: (@Composable () -> Unit)? = null
) { ) {
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -388,8 +348,6 @@ private fun IdeaCard(
InfoChip(label = "Premium", emphasis = true) InfoChip(label = "Premium", emphasis = true)
} }
} }
action?.invoke()
} }
} }
} }

View File

@ -3,11 +3,9 @@ package app.closer.ui.dates
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.closer.data.remote.FirestoreDateMemoryDataSource
import app.closer.data.repository.DateIdeaSeed import app.closer.data.repository.DateIdeaSeed
import app.closer.domain.model.DateIdea import app.closer.domain.model.DateIdea
import app.closer.domain.model.DateMatch import app.closer.domain.model.DateMatch
import app.closer.domain.model.DateMemory
import app.closer.domain.model.DateMatchSuggestion import app.closer.domain.model.DateMatchSuggestion
import app.closer.domain.model.DateSwipe import app.closer.domain.model.DateSwipe
import app.closer.domain.model.SwipeAction import app.closer.domain.model.SwipeAction
@ -18,11 +16,8 @@ import app.closer.domain.repository.UserRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -47,44 +42,16 @@ class DateMatchesViewModel @Inject constructor(
private val repository: DateMatchRepository, private val repository: DateMatchRepository,
private val authRepository: AuthRepository, private val authRepository: AuthRepository,
private val coupleRepository: CoupleRepository, private val coupleRepository: CoupleRepository,
private val userRepository: UserRepository, private val userRepository: UserRepository
private val memoryDataSource: FirestoreDateMemoryDataSource
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableStateFlow(DateMatchesUiState()) private val _uiState = MutableStateFlow(DateMatchesUiState())
val uiState: StateFlow<DateMatchesUiState> = _uiState.asStateFlow() val uiState: StateFlow<DateMatchesUiState> = _uiState.asStateFlow()
/** Emits the dateId to open its reflection screen right after a date is marked done. */
private val _markedDateId = MutableSharedFlow<String>(extraBufferCapacity = 1)
val markedDateId: SharedFlow<String> = _markedDateId.asSharedFlow()
init { init {
loadMatches() 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() { private fun loadMatches() {
viewModelScope.launch { viewModelScope.launch {
_uiState.value = DateMatchesUiState(isLoading = true) _uiState.value = DateMatchesUiState(isLoading = true)

View File

@ -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<DateMemoryRow> = 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<DateMemoriesUiState> = _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)
)
}
}
}

View File

@ -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<DateReflectionUiState> = _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()
}