feat: navigation, answer history screen + viewmodel, answer reveal, iOS navigation & question views
This commit is contained in:
parent
433d04d23c
commit
97acfaf702
|
|
@ -497,6 +497,7 @@ private val shellBackRoutes = setOf(
|
|||
AppRoute.QUESTION_COMPOSER,
|
||||
AppRoute.QUESTION_THREAD,
|
||||
AppRoute.ANSWER_REVEAL,
|
||||
AppRoute.ANSWER_HISTORY,
|
||||
AppRoute.CATEGORY_PICKER,
|
||||
AppRoute.SPIN_WHEEL,
|
||||
AppRoute.WHEEL_SESSION,
|
||||
|
|
|
|||
|
|
@ -70,8 +70,16 @@ fun AnswerHistoryScreen(
|
|||
|
||||
AnswerHistoryContent(
|
||||
state = state,
|
||||
onAnswerSelected = { onNavigate(AppRoute.answerReveal(it.questionId)) },
|
||||
onDailyQuestion = { onNavigate(AppRoute.DAILY_QUESTION) },
|
||||
onAnswerSelected = {
|
||||
if (state.isPairingLoaded) {
|
||||
onNavigate(if (state.isPaired) AppRoute.answerReveal(it.questionId) else AppRoute.CREATE_INVITE)
|
||||
}
|
||||
},
|
||||
onEmptyAction = {
|
||||
if (state.isPairingLoaded) {
|
||||
onNavigate(if (state.isPaired) AppRoute.DAILY_QUESTION else AppRoute.CREATE_INVITE)
|
||||
}
|
||||
},
|
||||
onDelete = viewModel::deleteAnswer
|
||||
)
|
||||
}
|
||||
|
|
@ -80,7 +88,7 @@ fun AnswerHistoryScreen(
|
|||
private fun AnswerHistoryContent(
|
||||
state: AnswerHistoryUiState,
|
||||
onAnswerSelected: (LocalAnswer) -> Unit,
|
||||
onDailyQuestion: () -> Unit,
|
||||
onEmptyAction: () -> Unit,
|
||||
onDelete: (String) -> Unit
|
||||
) {
|
||||
var pendingDelete by remember { mutableStateOf<LocalAnswer?>(null) }
|
||||
|
|
@ -159,11 +167,31 @@ private fun AnswerHistoryContent(
|
|||
|
||||
if (state.answers.isEmpty()) {
|
||||
item {
|
||||
val title = when {
|
||||
!state.isPairingLoaded -> "Checking your space"
|
||||
state.isPaired -> "Nothing here yet"
|
||||
else -> "Connect your partner"
|
||||
}
|
||||
val body = when {
|
||||
!state.isPairingLoaded -> "We are checking whether this private space is connected."
|
||||
state.isPaired -> "Answer today's question or pick a prompt from a pack. Every answer you save will wait for you here."
|
||||
else -> "Invite your partner to unlock shared reveals and build your private answer history together."
|
||||
}
|
||||
val actionLabel = when {
|
||||
!state.isPairingLoaded -> null
|
||||
state.isPaired -> "Today's question"
|
||||
else -> "Invite partner"
|
||||
}
|
||||
val onAction = if (state.isPairingLoaded) {
|
||||
onEmptyAction
|
||||
} else {
|
||||
null
|
||||
}
|
||||
EmptyState(
|
||||
title = "Nothing here yet",
|
||||
body = "Answer today's question or pick a prompt from a pack. Every answer you save will wait for you here.",
|
||||
actionLabel = "Today's question",
|
||||
onAction = onDailyQuestion,
|
||||
title = title,
|
||||
body = body,
|
||||
actionLabel = actionLabel,
|
||||
onAction = onAction,
|
||||
illustrationResId = R.drawable.illustration_couple_history
|
||||
)
|
||||
}
|
||||
|
|
@ -399,7 +427,7 @@ fun AnswerHistoryScreenPreview() {
|
|||
)
|
||||
),
|
||||
onAnswerSelected = {},
|
||||
onDailyQuestion = {},
|
||||
onEmptyAction = {},
|
||||
onDelete = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,36 +3,52 @@ package app.closer.ui.answers
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.closer.domain.model.LocalAnswer
|
||||
import app.closer.domain.repository.AuthRepository
|
||||
import app.closer.domain.repository.CoupleRepository
|
||||
import app.closer.domain.repository.LocalAnswerRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class AnswerHistoryUiState(
|
||||
val answers: List<LocalAnswer> = emptyList()
|
||||
val answers: List<LocalAnswer> = emptyList(),
|
||||
val isPaired: Boolean = false,
|
||||
val isPairingLoaded: Boolean = false
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class AnswerHistoryViewModel @Inject constructor(
|
||||
private val localAnswerRepository: LocalAnswerRepository
|
||||
private val localAnswerRepository: LocalAnswerRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
private val coupleRepository: CoupleRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(AnswerHistoryUiState())
|
||||
val uiState: StateFlow<AnswerHistoryUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
loadPairingState()
|
||||
viewModelScope.launch {
|
||||
localAnswerRepository.observeAnswers().collect { answers ->
|
||||
_uiState.value = AnswerHistoryUiState(
|
||||
answers = answers.sortedByDescending { it.updatedAt }
|
||||
)
|
||||
_uiState.update { it.copy(answers = answers.sortedByDescending { answer -> answer.updatedAt }) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadPairingState() {
|
||||
viewModelScope.launch {
|
||||
val userId = authRepository.currentUserId
|
||||
val isPaired = userId?.let {
|
||||
runCatching { coupleRepository.getCoupleForUser(it) }.getOrNull() != null
|
||||
} ?: false
|
||||
_uiState.update { it.copy(isPaired = isPaired, isPairingLoaded = true) }
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAnswer(questionId: String) {
|
||||
viewModelScope.launch {
|
||||
localAnswerRepository.deleteAnswer(questionId)
|
||||
|
|
|
|||
|
|
@ -64,6 +64,12 @@ fun AnswerRevealScreen(
|
|||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(state.isLoading, state.coupleId) {
|
||||
if (!state.isLoading && state.coupleId == null) {
|
||||
onNavigate(AppRoute.CREATE_INVITE)
|
||||
}
|
||||
}
|
||||
|
||||
AnswerRevealContent(
|
||||
state = state,
|
||||
questionId = questionId,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ struct ContentView: View {
|
|||
struct MainTabView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var selectedTab: Tab = .home
|
||||
@State private var showCreateInvite = false
|
||||
@State private var showAnswerHistory = false
|
||||
|
||||
enum Tab: Hashable {
|
||||
case home, dailyQuestion, play, questionPacks, settings
|
||||
|
|
@ -85,10 +87,10 @@ struct MainTabView: View {
|
|||
}
|
||||
.tint(.closerPrimary)
|
||||
.onReceive(NotificationCenter.default.publisher(for: .navigateToDailyQuestion)) { _ in
|
||||
selectedTab = .dailyQuestion
|
||||
routeToDailyOrInvite()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .navigateToReveal)) { _ in
|
||||
selectedTab = .dailyQuestion
|
||||
routeToDailyOrInvite()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .navigateToHome)) { _ in
|
||||
selectedTab = .home
|
||||
|
|
@ -103,11 +105,30 @@ struct MainTabView: View {
|
|||
selectedTab = .play
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .navigateToAnswerHistory)) { _ in
|
||||
selectedTab = .dailyQuestion
|
||||
if appState.currentUser?.coupleId == nil {
|
||||
showCreateInvite = true
|
||||
} else {
|
||||
showAnswerHistory = true
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .navigateToSettings)) { _ in
|
||||
selectedTab = .settings
|
||||
}
|
||||
.navigationDestination(isPresented: $showAnswerHistory) {
|
||||
AnswerHistoryView()
|
||||
}
|
||||
.fullScreenCover(isPresented: $showCreateInvite) {
|
||||
CreateInviteView()
|
||||
.environmentObject(appState)
|
||||
}
|
||||
}
|
||||
|
||||
private func routeToDailyOrInvite() {
|
||||
if appState.currentUser?.coupleId == nil {
|
||||
showCreateInvite = true
|
||||
} else {
|
||||
selectedTab = .dailyQuestion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -130,4 +151,4 @@ struct OnboardingFlow: View {
|
|||
|
||||
// MARK: - Import for Secret
|
||||
|
||||
import Foundation
|
||||
import Foundation
|
||||
|
|
|
|||
|
|
@ -293,10 +293,22 @@ struct AnswerRevealView: View {
|
|||
@EnvironmentObject var appState: AppState
|
||||
@State private var partnerAnswer: String?
|
||||
@State private var isLoading = true
|
||||
@State private var showCreateInvite = false
|
||||
|
||||
private var isPaired: Bool {
|
||||
appState.currentUser?.coupleId != nil
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: CloserSpacing.xxl) {
|
||||
if isLoading {
|
||||
if !isPaired {
|
||||
EmptyStateView(
|
||||
icon: "lock.fill",
|
||||
title: "Connect your partner",
|
||||
message: "Invite your partner to unlock shared reveals and private answers you can open together.",
|
||||
action: (title: "Invite partner", handler: { showCreateInvite = true })
|
||||
)
|
||||
} else if isLoading {
|
||||
LoadingView(message: "Loading answer...")
|
||||
} else if let answer = partnerAnswer {
|
||||
Image(systemName: "heart.fill")
|
||||
|
|
@ -329,6 +341,10 @@ struct AnswerRevealView: View {
|
|||
.closerPadding()
|
||||
.background(Color.closerBackground)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.fullScreenCover(isPresented: $showCreateInvite) {
|
||||
CreateInviteView()
|
||||
.environmentObject(appState)
|
||||
}
|
||||
.task {
|
||||
// Load partner's answer
|
||||
try? await Task.sleep(nanoseconds: 800_000_000)
|
||||
|
|
@ -340,8 +356,18 @@ struct AnswerRevealView: View {
|
|||
// MARK: - Answer History
|
||||
|
||||
struct AnswerHistoryView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@State private var answers: [Answer] = []
|
||||
@State private var isLoading = true
|
||||
@State private var showCreateInvite = false
|
||||
|
||||
private var isPaired: Bool {
|
||||
appState.currentUser?.coupleId != nil
|
||||
}
|
||||
|
||||
private var emptyAction: (title: String, handler: () -> Void)? {
|
||||
isPaired ? nil : (title: "Invite partner", handler: { showCreateInvite = true })
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
|
|
@ -351,25 +377,28 @@ struct AnswerHistoryView: View {
|
|||
} else if answers.isEmpty {
|
||||
EmptyStateView(
|
||||
icon: "clock.arrow.circlepath",
|
||||
title: "No Answers Yet",
|
||||
message: "Your answer history will appear here.",
|
||||
illustrationName: "illustration-couple-history"
|
||||
title: isPaired ? "No Answers Yet" : "Connect your partner",
|
||||
message: isPaired
|
||||
? "Your answer history will appear here."
|
||||
: "Invite your partner to unlock shared reveals and build your private answer history together.",
|
||||
illustrationName: "illustration-couple-history",
|
||||
action: emptyAction
|
||||
)
|
||||
.listRowBackground(Color.clear)
|
||||
} else {
|
||||
ForEach(answers) { answer in
|
||||
NavigationLink {
|
||||
AnswerRevealView(questionId: answer.questionId)
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(answer.answerText)
|
||||
.font(CloserFont.body)
|
||||
.foregroundColor(.closerText)
|
||||
Text(answer.createdAt, style: .date)
|
||||
.font(CloserFont.caption)
|
||||
.foregroundColor(.closerTextSecondary)
|
||||
if isPaired {
|
||||
NavigationLink {
|
||||
AnswerRevealView(questionId: answer.questionId)
|
||||
} label: {
|
||||
AnswerHistoryRow(answer: answer)
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
showCreateInvite = true
|
||||
} label: {
|
||||
AnswerHistoryRow(answer: answer)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -378,6 +407,10 @@ struct AnswerHistoryView: View {
|
|||
.background(Color.closerBackground)
|
||||
.navigationTitle("Answer History")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.fullScreenCover(isPresented: $showCreateInvite) {
|
||||
CreateInviteView()
|
||||
.environmentObject(appState)
|
||||
}
|
||||
.task {
|
||||
// Load answers
|
||||
try? await Task.sleep(nanoseconds: 500_000_000)
|
||||
|
|
@ -386,6 +419,22 @@ struct AnswerHistoryView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private struct AnswerHistoryRow: View {
|
||||
let answer: Answer
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(answer.answerText)
|
||||
.font(CloserFont.body)
|
||||
.foregroundColor(.closerText)
|
||||
Text(answer.createdAt, style: .date)
|
||||
.font(CloserFont.caption)
|
||||
.foregroundColor(.closerTextSecondary)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Question Pack Library
|
||||
|
||||
struct QuestionPackLibraryView: View {
|
||||
|
|
|
|||
Loading…
Reference in New Issue