Merge pull request #20 from garfiec/refactor/nav-host-viewmodel_stage2
refactor: NavHostViewModel stage 2 — extract banner and version check state holders
This commit is contained in:
commit
30f15dcdc9
3 changed files with 140 additions and 85 deletions
|
|
@ -0,0 +1,58 @@
|
||||||
|
package com.garfiec.librechat.shared.navigation
|
||||||
|
|
||||||
|
import co.touchlab.kermit.Logger
|
||||||
|
import com.garfiec.librechat.core.common.result.Result
|
||||||
|
import com.garfiec.librechat.core.data.repository.BannerRepository
|
||||||
|
import com.garfiec.librechat.core.model.Banner
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
|
class BannerStateHolder(
|
||||||
|
private val bannerRepository: BannerRepository,
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val _banners = MutableStateFlow<List<Banner>>(emptyList())
|
||||||
|
val banners: StateFlow<List<Banner>> = _banners.asStateFlow()
|
||||||
|
|
||||||
|
private val _dismissedBannerIds = MutableStateFlow<Set<String>>(emptySet())
|
||||||
|
val dismissedBannerIds: StateFlow<Set<String>> = _dismissedBannerIds.asStateFlow()
|
||||||
|
|
||||||
|
fun fetchBanners() {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val result = bannerRepository.getBanners()
|
||||||
|
if (result is Result.Success) {
|
||||||
|
val now = Clock.System.now()
|
||||||
|
_banners.value = result.data.filter { banner ->
|
||||||
|
val from = banner.displayFrom?.let {
|
||||||
|
runCatching { Instant.parse(it) }
|
||||||
|
.onFailure { e -> Logger.w(e) { "Failed to parse banner displayFrom: $it" } }
|
||||||
|
.getOrNull()
|
||||||
|
}
|
||||||
|
val to = banner.displayTo?.let {
|
||||||
|
runCatching { Instant.parse(it) }
|
||||||
|
.onFailure { e -> Logger.w(e) { "Failed to parse banner displayTo: $it" } }
|
||||||
|
.getOrNull()
|
||||||
|
}
|
||||||
|
val afterStart = from == null || now >= from
|
||||||
|
val beforeEnd = to == null || now < to
|
||||||
|
afterStart && beforeEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.w(e) { "Failed to fetch banners" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissBanner(bannerId: String) {
|
||||||
|
_dismissedBannerIds.update { it + bannerId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,9 +23,7 @@ import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import co.touchlab.kermit.Logger
|
import co.touchlab.kermit.Logger
|
||||||
|
|
@ -64,19 +62,10 @@ data class DrawerUiState(
|
||||||
val hasMore: Boolean = true,
|
val hasMore: Boolean = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* UI state for a backend version mismatch warning.
|
|
||||||
*/
|
|
||||||
@Immutable
|
|
||||||
data class VersionMismatchState(
|
|
||||||
val supportedVersion: String,
|
|
||||||
val backendVersion: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
class NavHostViewModel(
|
class NavHostViewModel(
|
||||||
private val authRepository: AuthRepository,
|
private val authRepository: AuthRepository,
|
||||||
private val bannerRepository: BannerRepository,
|
bannerRepository: BannerRepository,
|
||||||
private val configRepository: ConfigRepository,
|
configRepository: ConfigRepository,
|
||||||
private val conversationRepository: ConversationRepository,
|
private val conversationRepository: ConversationRepository,
|
||||||
private val tokenManager: TokenManager,
|
private val tokenManager: TokenManager,
|
||||||
private val settingsDataStore: com.garfiec.librechat.core.data.datastore.SettingsDataStore,
|
private val settingsDataStore: com.garfiec.librechat.core.data.datastore.SettingsDataStore,
|
||||||
|
|
@ -86,11 +75,13 @@ class NavHostViewModel(
|
||||||
private const val SEARCH_DEBOUNCE_MS = 300L
|
private const val SEARCH_DEBOUNCE_MS = 300L
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val bannerStateHolder = BannerStateHolder(bannerRepository, viewModelScope)
|
||||||
|
private val versionCheckStateHolder = VersionCheckStateHolder(configRepository, settingsDataStore, viewModelScope)
|
||||||
|
|
||||||
private val _isLoggedIn = MutableStateFlow(tokenManager.isAuthenticated)
|
private val _isLoggedIn = MutableStateFlow(tokenManager.isAuthenticated)
|
||||||
val isLoggedIn: StateFlow<Boolean> = _isLoggedIn.asStateFlow()
|
val isLoggedIn: StateFlow<Boolean> = _isLoggedIn.asStateFlow()
|
||||||
|
|
||||||
private val _versionMismatch = MutableStateFlow<VersionMismatchState?>(null)
|
val versionMismatch: StateFlow<VersionMismatchState?> = versionCheckStateHolder.versionMismatch
|
||||||
val versionMismatch: StateFlow<VersionMismatchState?> = _versionMismatch.asStateFlow()
|
|
||||||
|
|
||||||
private val _recentConversations = MutableStateFlow<List<Conversation>>(emptyList())
|
private val _recentConversations = MutableStateFlow<List<Conversation>>(emptyList())
|
||||||
private val _groupedConversations = MutableStateFlow<List<Pair<String, List<Conversation>>>>(emptyList())
|
private val _groupedConversations = MutableStateFlow<List<Pair<String, List<Conversation>>>>(emptyList())
|
||||||
|
|
@ -103,11 +94,8 @@ class NavHostViewModel(
|
||||||
private val _isLoadingMore = MutableStateFlow(false)
|
private val _isLoadingMore = MutableStateFlow(false)
|
||||||
private val _isRefreshing = MutableStateFlow(false)
|
private val _isRefreshing = MutableStateFlow(false)
|
||||||
|
|
||||||
private val _banners = MutableStateFlow<List<Banner>>(emptyList())
|
val banners: StateFlow<List<Banner>> = bannerStateHolder.banners
|
||||||
val banners: StateFlow<List<Banner>> = _banners.asStateFlow()
|
val dismissedBannerIds: StateFlow<Set<String>> = bannerStateHolder.dismissedBannerIds
|
||||||
|
|
||||||
private val _dismissedBannerIds = MutableStateFlow<Set<String>>(emptySet())
|
|
||||||
val dismissedBannerIds: StateFlow<Set<String>> = _dismissedBannerIds.asStateFlow()
|
|
||||||
|
|
||||||
val sessionExpired: SharedFlow<Unit> = tokenManager.sessionExpiredFlow
|
val sessionExpired: SharedFlow<Unit> = tokenManager.sessionExpiredFlow
|
||||||
|
|
||||||
|
|
@ -167,7 +155,7 @@ class NavHostViewModel(
|
||||||
val loggedIn = authRepository.isLoggedIn()
|
val loggedIn = authRepository.isLoggedIn()
|
||||||
_isLoggedIn.value = loggedIn
|
_isLoggedIn.value = loggedIn
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
checkBackendVersion()
|
versionCheckStateHolder.checkBackendVersion()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.w(e) { "Failed to check auth state on init" }
|
Logger.w(e) { "Failed to check auth state on init" }
|
||||||
|
|
@ -176,7 +164,7 @@ class NavHostViewModel(
|
||||||
observeConversations()
|
observeConversations()
|
||||||
observeSearchQuery()
|
observeSearchQuery()
|
||||||
observeBookmarks()
|
observeBookmarks()
|
||||||
fetchBanners()
|
bannerStateHolder.fetchBanners()
|
||||||
loadInitialConversations()
|
loadInitialConversations()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,8 +221,8 @@ class NavHostViewModel(
|
||||||
fun onAuthComplete() {
|
fun onAuthComplete() {
|
||||||
_isLoggedIn.value = true
|
_isLoggedIn.value = true
|
||||||
loadInitialConversations()
|
loadInitialConversations()
|
||||||
fetchBanners()
|
bannerStateHolder.fetchBanners()
|
||||||
checkBackendVersion()
|
versionCheckStateHolder.checkBackendVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshConversations() {
|
fun refreshConversations() {
|
||||||
|
|
@ -304,34 +292,6 @@ class NavHostViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchBanners() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
val result = bannerRepository.getBanners()
|
|
||||||
if (result is Result.Success) {
|
|
||||||
val now = Clock.System.now()
|
|
||||||
_banners.value = result.data.filter { banner ->
|
|
||||||
val from = banner.displayFrom?.let {
|
|
||||||
runCatching { Instant.parse(it) }
|
|
||||||
.onFailure { e -> Logger.w(e) { "Failed to parse banner displayFrom: $it" } }
|
|
||||||
.getOrNull()
|
|
||||||
}
|
|
||||||
val to = banner.displayTo?.let {
|
|
||||||
runCatching { Instant.parse(it) }
|
|
||||||
.onFailure { e -> Logger.w(e) { "Failed to parse banner displayTo: $it" } }
|
|
||||||
.getOrNull()
|
|
||||||
}
|
|
||||||
val afterStart = from == null || now >= from
|
|
||||||
val beforeEnd = to == null || now < to
|
|
||||||
afterStart && beforeEnd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.w(e) { "Failed to fetch banners" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setTabletSidebarOpen(open: Boolean) {
|
fun setTabletSidebarOpen(open: Boolean) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
settingsDataStore.setTabletSidebarOpen(open)
|
settingsDataStore.setTabletSidebarOpen(open)
|
||||||
|
|
@ -339,45 +299,15 @@ class NavHostViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissBanner(bannerId: String) {
|
fun dismissBanner(bannerId: String) {
|
||||||
_dismissedBannerIds.update { it + bannerId }
|
bannerStateHolder.dismissBanner(bannerId)
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkBackendVersion() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
when (val result = configRepository.checkBackendVersion()) {
|
|
||||||
is Result.Success -> {
|
|
||||||
val checkResult = result.data
|
|
||||||
val detectedVersion = checkResult.backendVersion
|
|
||||||
if (!checkResult.isCompatible && detectedVersion != null) {
|
|
||||||
val dismissedVersion = settingsDataStore.dismissedVersionWarning.first()
|
|
||||||
if (dismissedVersion != detectedVersion) {
|
|
||||||
_versionMismatch.value = VersionMismatchState(
|
|
||||||
supportedVersion = checkResult.supportedVersion,
|
|
||||||
backendVersion = detectedVersion,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is Result.Error -> {
|
|
||||||
Logger.w(result.exception) { "Failed to check backend version: ${result.message}" }
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissVersionWarning() {
|
fun dismissVersionWarning() {
|
||||||
_versionMismatch.value = null
|
versionCheckStateHolder.dismissVersionWarning()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissVersionWarningPermanently() {
|
fun dismissVersionWarningPermanently() {
|
||||||
val backendVersion = _versionMismatch.value?.backendVersion
|
versionCheckStateHolder.dismissVersionWarningPermanently()
|
||||||
_versionMismatch.value = null
|
|
||||||
if (backendVersion != null) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
settingsDataStore.setDismissedVersionWarning(backendVersion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun logout() {
|
fun logout() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package com.garfiec.librechat.shared.navigation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import co.touchlab.kermit.Logger
|
||||||
|
import com.garfiec.librechat.core.common.result.Result
|
||||||
|
import com.garfiec.librechat.core.data.datastore.SettingsDataStore
|
||||||
|
import com.garfiec.librechat.core.data.repository.ConfigRepository
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class VersionMismatchState(
|
||||||
|
val supportedVersion: String,
|
||||||
|
val backendVersion: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
class VersionCheckStateHolder(
|
||||||
|
private val configRepository: ConfigRepository,
|
||||||
|
private val settingsDataStore: SettingsDataStore,
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val _versionMismatch = MutableStateFlow<VersionMismatchState?>(null)
|
||||||
|
val versionMismatch: StateFlow<VersionMismatchState?> = _versionMismatch.asStateFlow()
|
||||||
|
|
||||||
|
fun checkBackendVersion() {
|
||||||
|
scope.launch {
|
||||||
|
when (val result = configRepository.checkBackendVersion()) {
|
||||||
|
is Result.Success -> {
|
||||||
|
val checkResult = result.data
|
||||||
|
val detectedVersion = checkResult.backendVersion
|
||||||
|
if (!checkResult.isCompatible && detectedVersion != null) {
|
||||||
|
val dismissedVersion = settingsDataStore.dismissedVersionWarning.first()
|
||||||
|
if (dismissedVersion != detectedVersion) {
|
||||||
|
_versionMismatch.value = VersionMismatchState(
|
||||||
|
supportedVersion = checkResult.supportedVersion,
|
||||||
|
backendVersion = detectedVersion,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Result.Error -> {
|
||||||
|
Logger.w(result.exception) { "Failed to check backend version: ${result.message}" }
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissVersionWarning() {
|
||||||
|
_versionMismatch.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissVersionWarningPermanently() {
|
||||||
|
val backendVersion = _versionMismatch.value?.backendVersion
|
||||||
|
_versionMismatch.value = null
|
||||||
|
if (backendVersion != null) {
|
||||||
|
scope.launch {
|
||||||
|
settingsDataStore.setDismissedVersionWarning(backendVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue