refactor(android): update question flow and navigation patterns (batch 6)
This commit is contained in:
parent
67251537eb
commit
2a5cd28397
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue