refactor(android): update question flow and navigation patterns (batch 6)

This commit is contained in:
null 2026-06-20 17:17:51 -05:00
parent 67251537eb
commit 2a5cd28397
4 changed files with 42 additions and 9 deletions

View File

@ -202,7 +202,7 @@ fun AppNavigation(
route = AppRoute.DAILY_QUESTION,
deepLinks = listOf(navDeepLink { uriPattern = "closer://closer.app/daily_question" })
) {
DailyQuestionScreen(onNavigate = navigateRoute)
DailyQuestionScreen(onNavigate = navigateRoute, onBack = navigateBackOrHome)
}
composable(route = AppRoute.QUESTION_PACKS) {
QuestionPackLibraryScreen(onNavigate = navigateRoute)

View File

@ -11,10 +11,17 @@ import app.closer.domain.model.Question
@Composable
fun DailyQuestionScreen(
onNavigate: (String) -> Unit = {},
onBack: () -> Unit = {},
viewModel: DailyQuestionViewModel = hiltViewModel()
) {
val state by viewModel.uiState.collectAsState()
// Reveal is only offered once both partners have answered — the reveal screen enforces
// this too, but surfacing the button early is misleading and should be avoided.
val revealRoute: (() -> Unit)? = if (state.submitted && state.partnerHasAnswered) {
state.question?.let { q -> { onNavigate(AppRoute.answerReveal(q.id)) } }
} else null
LocalQuestionContent(
state = state,
title = "One question, enough space",
@ -25,20 +32,17 @@ fun DailyQuestionScreen(
if (coupleId != null) {
onNavigate(AppRoute.questionThread(coupleId, question.id))
} else {
// Discussing requires a paired partner; send unpaired users to invite one.
onNavigate(AppRoute.CREATE_INVITE)
}
},
onSecondaryRoute = state.question?.let {
{ onNavigate(AppRoute.answerReveal(it.id)) }
},
secondaryRouteLabel = "Reveal",
onSecondaryRoute = revealRoute,
secondaryRouteLabel = if (revealRoute != null) "Reveal" else null,
onWrittenTextChanged = viewModel::updateWrittenText,
onOptionToggled = viewModel::toggleOption,
onScaleChanged = viewModel::updateScale,
onSubmit = viewModel::submitAnswer,
canSubmit = viewModel.canSubmit(),
onRefresh = viewModel::loadDailyQuestion
onBack = onBack
)
}

View File

@ -28,7 +28,8 @@ data class LocalQuestionUiState(
val submitted: Boolean = false,
val pendingWrittenText: String = "",
val pendingSelectedOptionIds: List<String> = emptyList(),
val pendingScaleValue: Int = 3
val pendingScaleValue: Int = 3,
val partnerHasAnswered: Boolean = false
)
@HiltViewModel
@ -55,12 +56,16 @@ class DailyQuestionViewModel @Inject constructor(
val today = FirestoreAnswerDataSource.todayLocalDateString()
val (coupleId, question) = loadCoupleAndQuestion(today)
val answer = question?.let { localAnswerRepository.getAnswer(it.id) }
val partnerHasAnswered = coupleId?.let {
runCatching { checkPartnerAnswered(it, today) }.getOrDefault(false)
} ?: false
_uiState.value = LocalQuestionUiState(
isLoading = false,
question = question,
coupleId = coupleId,
dailyQuestionDate = today,
pendingScaleValue = defaultScaleValue(question)
pendingScaleValue = defaultScaleValue(question),
partnerHasAnswered = partnerHasAnswered
).withLocalAnswer(answer)
} catch (e: Exception) {
crashReporter.recordException(e)
@ -189,6 +194,17 @@ class DailyQuestionViewModel @Inject constructor(
}.onFailure {
crashReporter.recordException(it)
}
// After submitting, refresh partner-answered status so the reveal button appears
// immediately if the partner answered while the user was composing.
val partnerHasAnswered = runCatching { checkPartnerAnswered(coupleId, dailyQuestionDate) }.getOrDefault(false)
_uiState.update { it.copy(partnerHasAnswered = partnerHasAnswered) }
}
private suspend fun checkPartnerAnswered(coupleId: String, date: String): Boolean {
val userId = authRepository.currentUserId ?: return false
val couple = runCatching { coupleRepository.getCoupleForUser(userId) }.getOrNull() ?: return false
val partnerId = couple.userIds.firstOrNull { it != userId } ?: return false
return firestoreAnswerDataSource.getAnswerForUser(coupleId, partnerId, date) != null
}
}

View File

@ -23,11 +23,15 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
@ -71,6 +75,7 @@ fun LocalQuestionContent(
onSubmit: () -> Unit,
canSubmit: Boolean,
onRefresh: (() -> Unit)? = null,
onBack: (() -> Unit)? = null,
modifier: Modifier = Modifier
) {
val background = closerBackgroundBrush()
@ -89,6 +94,14 @@ fun LocalQuestionContent(
.padding(horizontal = 20.dp, vertical = 18.dp),
verticalArrangement = Arrangement.spacedBy(18.dp)
) {
if (onBack != null) {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back"
)
}
}
LocalQuestionHeader(title = title, subtitle = subtitle)
when {