diff --git a/shared/src/commonMain/kotlin/com/garfiec/librechat/shared/navigation/BannerStateHolder.kt b/shared/src/commonMain/kotlin/com/garfiec/librechat/shared/navigation/BannerStateHolder.kt new file mode 100644 index 0000000..92ee1dc --- /dev/null +++ b/shared/src/commonMain/kotlin/com/garfiec/librechat/shared/navigation/BannerStateHolder.kt @@ -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>(emptyList()) + val banners: StateFlow> = _banners.asStateFlow() + + private val _dismissedBannerIds = MutableStateFlow>(emptySet()) + val dismissedBannerIds: StateFlow> = _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 } + } +} diff --git a/shared/src/commonMain/kotlin/com/garfiec/librechat/shared/navigation/NavHostViewModel.kt b/shared/src/commonMain/kotlin/com/garfiec/librechat/shared/navigation/NavHostViewModel.kt index 1f85428..c60359d 100644 --- a/shared/src/commonMain/kotlin/com/garfiec/librechat/shared/navigation/NavHostViewModel.kt +++ b/shared/src/commonMain/kotlin/com/garfiec/librechat/shared/navigation/NavHostViewModel.kt @@ -23,9 +23,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.launch import co.touchlab.kermit.Logger @@ -64,19 +62,10 @@ data class DrawerUiState( val hasMore: Boolean = true, ) -/** - * UI state for a backend version mismatch warning. - */ -@Immutable -data class VersionMismatchState( - val supportedVersion: String, - val backendVersion: String, -) - class NavHostViewModel( private val authRepository: AuthRepository, - private val bannerRepository: BannerRepository, - private val configRepository: ConfigRepository, + bannerRepository: BannerRepository, + configRepository: ConfigRepository, private val conversationRepository: ConversationRepository, private val tokenManager: TokenManager, private val settingsDataStore: com.garfiec.librechat.core.data.datastore.SettingsDataStore, @@ -86,11 +75,13 @@ class NavHostViewModel( 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) val isLoggedIn: StateFlow = _isLoggedIn.asStateFlow() - private val _versionMismatch = MutableStateFlow(null) - val versionMismatch: StateFlow = _versionMismatch.asStateFlow() + val versionMismatch: StateFlow = versionCheckStateHolder.versionMismatch private val _recentConversations = MutableStateFlow>(emptyList()) private val _groupedConversations = MutableStateFlow>>>(emptyList()) @@ -103,11 +94,8 @@ class NavHostViewModel( private val _isLoadingMore = MutableStateFlow(false) private val _isRefreshing = MutableStateFlow(false) - private val _banners = MutableStateFlow>(emptyList()) - val banners: StateFlow> = _banners.asStateFlow() - - private val _dismissedBannerIds = MutableStateFlow>(emptySet()) - val dismissedBannerIds: StateFlow> = _dismissedBannerIds.asStateFlow() + val banners: StateFlow> = bannerStateHolder.banners + val dismissedBannerIds: StateFlow> = bannerStateHolder.dismissedBannerIds val sessionExpired: SharedFlow = tokenManager.sessionExpiredFlow @@ -167,7 +155,7 @@ class NavHostViewModel( val loggedIn = authRepository.isLoggedIn() _isLoggedIn.value = loggedIn if (loggedIn) { - checkBackendVersion() + versionCheckStateHolder.checkBackendVersion() } } catch (e: Exception) { Logger.w(e) { "Failed to check auth state on init" } @@ -176,7 +164,7 @@ class NavHostViewModel( observeConversations() observeSearchQuery() observeBookmarks() - fetchBanners() + bannerStateHolder.fetchBanners() loadInitialConversations() } @@ -233,8 +221,8 @@ class NavHostViewModel( fun onAuthComplete() { _isLoggedIn.value = true loadInitialConversations() - fetchBanners() - checkBackendVersion() + bannerStateHolder.fetchBanners() + versionCheckStateHolder.checkBackendVersion() } 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) { viewModelScope.launch { settingsDataStore.setTabletSidebarOpen(open) @@ -339,45 +299,15 @@ class NavHostViewModel( } fun dismissBanner(bannerId: String) { - _dismissedBannerIds.update { it + 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 -> {} - } - } + bannerStateHolder.dismissBanner(bannerId) } fun dismissVersionWarning() { - _versionMismatch.value = null + versionCheckStateHolder.dismissVersionWarning() } fun dismissVersionWarningPermanently() { - val backendVersion = _versionMismatch.value?.backendVersion - _versionMismatch.value = null - if (backendVersion != null) { - viewModelScope.launch { - settingsDataStore.setDismissedVersionWarning(backendVersion) - } - } + versionCheckStateHolder.dismissVersionWarningPermanently() } fun logout() { diff --git a/shared/src/commonMain/kotlin/com/garfiec/librechat/shared/navigation/VersionCheckStateHolder.kt b/shared/src/commonMain/kotlin/com/garfiec/librechat/shared/navigation/VersionCheckStateHolder.kt new file mode 100644 index 0000000..3f88d64 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/garfiec/librechat/shared/navigation/VersionCheckStateHolder.kt @@ -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(null) + val versionMismatch: StateFlow = _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) + } + } + } +}