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 fbfb6b7a92
commit eebdee6120
4 changed files with 42 additions and 9 deletions

View File

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

View File

@ -11,10 +11,17 @@ import app.closer.domain.model.Question
@Composable @Composable
fun DailyQuestionScreen( fun DailyQuestionScreen(
onNavigate: (String) -> Unit = {}, onNavigate: (String) -> Unit = {},
onBack: () -> Unit = {},
viewModel: DailyQuestionViewModel = hiltViewModel() viewModel: DailyQuestionViewModel = hiltViewModel()
) { ) {
val state by viewModel.uiState.collectAsState() 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( LocalQuestionContent(
state = state, state = state,
title = "One question, enough space", title = "One question, enough space",
@ -25,20 +32,17 @@ fun DailyQuestionScreen(
if (coupleId != null) { if (coupleId != null) {
onNavigate(AppRoute.questionThread(coupleId, question.id)) onNavigate(AppRoute.questionThread(coupleId, question.id))
} else { } else {
// Discussing requires a paired partner; send unpaired users to invite one.
onNavigate(AppRoute.CREATE_INVITE) onNavigate(AppRoute.CREATE_INVITE)
} }
}, },
onSecondaryRoute = state.question?.let { onSecondaryRoute = revealRoute,
{ onNavigate(AppRoute.answerReveal(it.id)) } secondaryRouteLabel = if (revealRoute != null) "Reveal" else null,
},
secondaryRouteLabel = "Reveal",
onWrittenTextChanged = viewModel::updateWrittenText, onWrittenTextChanged = viewModel::updateWrittenText,
onOptionToggled = viewModel::toggleOption, onOptionToggled = viewModel::toggleOption,
onScaleChanged = viewModel::updateScale, onScaleChanged = viewModel::updateScale,
onSubmit = viewModel::submitAnswer, onSubmit = viewModel::submitAnswer,
canSubmit = viewModel.canSubmit(), canSubmit = viewModel.canSubmit(),
onRefresh = viewModel::loadDailyQuestion onBack = onBack
) )
} }

View File

@ -28,7 +28,8 @@ data class LocalQuestionUiState(
val submitted: Boolean = false, val submitted: Boolean = false,
val pendingWrittenText: String = "", val pendingWrittenText: String = "",
val pendingSelectedOptionIds: List<String> = emptyList(), val pendingSelectedOptionIds: List<String> = emptyList(),
val pendingScaleValue: Int = 3 val pendingScaleValue: Int = 3,
val partnerHasAnswered: Boolean = false
) )
@HiltViewModel @HiltViewModel
@ -55,12 +56,16 @@ class DailyQuestionViewModel @Inject constructor(
val today = FirestoreAnswerDataSource.todayLocalDateString() val today = FirestoreAnswerDataSource.todayLocalDateString()
val (coupleId, question) = loadCoupleAndQuestion(today) val (coupleId, question) = loadCoupleAndQuestion(today)
val answer = question?.let { localAnswerRepository.getAnswer(it.id) } val answer = question?.let { localAnswerRepository.getAnswer(it.id) }
val partnerHasAnswered = coupleId?.let {
runCatching { checkPartnerAnswered(it, today) }.getOrDefault(false)
} ?: false
_uiState.value = LocalQuestionUiState( _uiState.value = LocalQuestionUiState(
isLoading = false, isLoading = false,
question = question, question = question,
coupleId = coupleId, coupleId = coupleId,
dailyQuestionDate = today, dailyQuestionDate = today,
pendingScaleValue = defaultScaleValue(question) pendingScaleValue = defaultScaleValue(question),
partnerHasAnswered = partnerHasAnswered
).withLocalAnswer(answer) ).withLocalAnswer(answer)
} catch (e: Exception) { } catch (e: Exception) {
crashReporter.recordException(e) crashReporter.recordException(e)
@ -189,6 +194,17 @@ class DailyQuestionViewModel @Inject constructor(
}.onFailure { }.onFailure {
crashReporter.recordException(it) 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.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll 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.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@ -71,6 +75,7 @@ fun LocalQuestionContent(
onSubmit: () -> Unit, onSubmit: () -> Unit,
canSubmit: Boolean, canSubmit: Boolean,
onRefresh: (() -> Unit)? = null, onRefresh: (() -> Unit)? = null,
onBack: (() -> Unit)? = null,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val background = closerBackgroundBrush() val background = closerBackgroundBrush()
@ -89,6 +94,14 @@ fun LocalQuestionContent(
.padding(horizontal = 20.dp, vertical = 18.dp), .padding(horizontal = 20.dp, vertical = 18.dp),
verticalArrangement = Arrangement.spacedBy(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) LocalQuestionHeader(title = title, subtitle = subtitle)
when { when {