Closer/app/src/main/java/app/closer/ui/dates/DateMatchViewModel.kt

149 lines
5.8 KiB
Kotlin

package app.closer.ui.dates
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.closer.domain.model.DateIdea
import app.closer.domain.model.DateMatch
import app.closer.domain.model.DateSwipe
import app.closer.domain.model.SwipeAction
import app.closer.domain.repository.AuthRepository
import app.closer.domain.repository.CoupleRepository
import app.closer.domain.repository.DateMatchRepository
import app.closer.domain.repository.UserRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
data class DateMatchUiState(
val isLoading: Boolean = true,
val error: String? = null,
val coupleId: String? = null,
val currentUserId: String? = null,
val partnerName: String? = null,
val dateIdeas: List<DateIdea> = emptyList(),
val ownSwipes: Map<String, SwipeAction> = emptyMap(),
val matches: List<DateMatch> = emptyList(),
val currentIndex: Int = 0,
val justMatched: DateMatch? = null
) {
val currentIdea: DateIdea? get() = dateIdeas.getOrNull(currentIndex)
val nextIdea: DateIdea? get() = dateIdeas.getOrNull(currentIndex + 1)
val hasMore: Boolean get() = currentIndex < dateIdeas.size
fun userAction(ideaId: String): SwipeAction? = ownSwipes[ideaId]
}
@HiltViewModel
class DateMatchViewModel @Inject constructor(
private val repository: DateMatchRepository,
private val authRepository: AuthRepository,
private val coupleRepository: CoupleRepository,
private val userRepository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(DateMatchUiState())
val uiState: StateFlow<DateMatchUiState> = _uiState.asStateFlow()
init {
loadDateMatch()
}
private fun loadDateMatch() {
viewModelScope.launch {
_uiState.value = DateMatchUiState(isLoading = true)
try {
val uid = authRepository.currentUserId
val couple = uid?.let { runCatching { coupleRepository.getCoupleForUser(it) }.getOrNull() }
val partnerName = couple?.userIds?.firstOrNull { it != uid }?.let { partnerId ->
runCatching { userRepository.getUser(partnerId)?.displayName }.getOrNull()
}
val ideas = repository.getDateIdeas()
_uiState.value = DateMatchUiState(
isLoading = false,
coupleId = couple?.id,
currentUserId = uid,
partnerName = partnerName,
dateIdeas = ideas
)
if (couple != null && uid != null) {
observeData(couple.id, uid)
}
} catch (e: Exception) {
_uiState.value = DateMatchUiState(
isLoading = false,
error = e.message ?: "Could not load date ideas."
)
}
}
}
private fun observeData(coupleId: String, userId: String) {
val swipesFlow: Flow<List<DateSwipe>> = repository.observeOwnSwipes(coupleId, userId)
val matchesFlow: Flow<List<DateMatch>> = repository.observeMatches(coupleId)
viewModelScope.launch {
// Matches are created server-side and stream in via observeMatches.
// Track ids already seen so the "It's a match!" celebration only fires
// for matches created after this screen started observing — not for
// matches that already existed on load.
var knownMatchIds: Set<String>? = null
combine(swipesFlow, matchesFlow) { swipes, matches -> swipes to matches }
.collect { (swipes, matches) ->
val newMatch = knownMatchIds?.let { known ->
matches.firstOrNull { it.id !in known }
}
knownMatchIds = matches.mapTo(mutableSetOf()) { it.id }
_uiState.update { state ->
state.copy(
ownSwipes = swipes.associate { it.dateIdeaId to it.action },
matches = matches,
justMatched = newMatch ?: state.justMatched
)
}
}
}
}
fun swipeCurrent(action: SwipeAction) {
val current = _uiState.value.currentIdea ?: return
val coupleId = _uiState.value.coupleId ?: return
val userId = _uiState.value.currentUserId ?: return
viewModelScope.launch {
_uiState.update { it.copy(justMatched = null) }
val result = repository.recordSwipe(coupleId, userId, current.id, action)
result.fold(
onSuccess = {
// Advance to the next idea. A resulting match (if both partners
// loved this idea) is created server-side and surfaces via the
// observeMatches flow, which sets justMatched.
_uiState.update { it.copy(currentIndex = it.currentIndex + 1) }
},
onFailure = { err ->
_uiState.update {
it.copy(error = err.message ?: "Could not save swipe.")
}
}
)
}
}
fun skipCurrent() = swipeCurrent(SwipeAction.SKIP)
fun maybeCurrent() = swipeCurrent(SwipeAction.MAYBE)
fun loveCurrent() = swipeCurrent(SwipeAction.LOVE)
fun dismissJustMatched() {
_uiState.update { it.copy(justMatched = null) }
}
fun retry() = loadDateMatch()
}