Merge pull request #19 from garfiec/refactor/nav-host-viewmodel_stage1
refactor: NavHostViewModel stage 1 — low-hanging fruit cleanup
This commit is contained in:
commit
6f86a2b00a
1 changed files with 73 additions and 61 deletions
|
|
@ -14,16 +14,19 @@ import com.garfiec.librechat.core.model.Banner
|
||||||
import com.garfiec.librechat.core.model.Conversation
|
import com.garfiec.librechat.core.model.Conversation
|
||||||
import com.garfiec.librechat.core.model.EModelEndpoint
|
import com.garfiec.librechat.core.model.EModelEndpoint
|
||||||
import com.garfiec.librechat.core.network.client.TokenManager
|
import com.garfiec.librechat.core.network.client.TokenManager
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
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.launch
|
import kotlinx.coroutines.launch
|
||||||
import co.touchlab.kermit.Logger
|
import co.touchlab.kermit.Logger
|
||||||
import com.garfiec.librechat.core.common.extensions.firstBlocking
|
import com.garfiec.librechat.core.common.extensions.firstBlocking
|
||||||
|
|
@ -79,6 +82,10 @@ class NavHostViewModel(
|
||||||
private val settingsDataStore: com.garfiec.librechat.core.data.datastore.SettingsDataStore,
|
private val settingsDataStore: com.garfiec.librechat.core.data.datastore.SettingsDataStore,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SEARCH_DEBOUNCE_MS = 300L
|
||||||
|
}
|
||||||
|
|
||||||
private val _isLoggedIn = MutableStateFlow(tokenManager.isAuthenticated)
|
private val _isLoggedIn = MutableStateFlow(tokenManager.isAuthenticated)
|
||||||
val isLoggedIn: StateFlow<Boolean> = _isLoggedIn.asStateFlow()
|
val isLoggedIn: StateFlow<Boolean> = _isLoggedIn.asStateFlow()
|
||||||
|
|
||||||
|
|
@ -128,8 +135,6 @@ class NavHostViewModel(
|
||||||
_selectedSettingsCategory.value = category
|
_selectedSettingsCategory.value = category
|
||||||
}
|
}
|
||||||
|
|
||||||
private var searchJob: Job? = null
|
|
||||||
|
|
||||||
val drawerUiState: StateFlow<DrawerUiState> = combine(
|
val drawerUiState: StateFlow<DrawerUiState> = combine(
|
||||||
combine(
|
combine(
|
||||||
_groupedConversations,
|
_groupedConversations,
|
||||||
|
|
@ -169,6 +174,7 @@ class NavHostViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
observeConversations()
|
observeConversations()
|
||||||
|
observeSearchQuery()
|
||||||
observeBookmarks()
|
observeBookmarks()
|
||||||
fetchBanners()
|
fetchBanners()
|
||||||
loadInitialConversations()
|
loadInitialConversations()
|
||||||
|
|
@ -194,15 +200,24 @@ class NavHostViewModel(
|
||||||
|
|
||||||
private fun loadInitialConversations() {
|
private fun loadInitialConversations() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
loadPage(cursor = null)
|
||||||
val result = conversationRepository.loadNextPage(cursor = null)
|
}
|
||||||
if (result is Result.Success) {
|
}
|
||||||
nextCursor = result.data
|
|
||||||
_hasMore.value = result.data != null
|
private suspend fun loadPage(cursor: String?): Boolean {
|
||||||
}
|
return try {
|
||||||
} catch (e: Exception) {
|
val result = conversationRepository.loadNextPage(cursor = cursor)
|
||||||
Logger.w(e) { "Failed to load initial conversations" }
|
if (result is Result.Success) {
|
||||||
|
nextCursor = result.data
|
||||||
|
_hasMore.value = result.data != null
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
Logger.w { "Failed to load conversations page" }
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.w(e) { "Failed to load conversations page" }
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,19 +225,8 @@ class NavHostViewModel(
|
||||||
if (_isLoadingMore.value || !_hasMore.value) return
|
if (_isLoadingMore.value || !_hasMore.value) return
|
||||||
_isLoadingMore.value = true
|
_isLoadingMore.value = true
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
loadPage(cursor = nextCursor)
|
||||||
val result = conversationRepository.loadNextPage(cursor = nextCursor)
|
_isLoadingMore.value = false
|
||||||
if (result is Result.Success) {
|
|
||||||
nextCursor = result.data
|
|
||||||
_hasMore.value = result.data != null
|
|
||||||
} else {
|
|
||||||
Logger.w { "Failed to load more conversations" }
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.w(e) { "Failed to load more conversations" }
|
|
||||||
} finally {
|
|
||||||
_isLoadingMore.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -236,19 +240,10 @@ class NavHostViewModel(
|
||||||
fun refreshConversations() {
|
fun refreshConversations() {
|
||||||
_isRefreshing.value = true
|
_isRefreshing.value = true
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
nextCursor = null
|
||||||
nextCursor = null
|
_hasMore.value = true
|
||||||
_hasMore.value = true
|
loadPage(cursor = null)
|
||||||
val result = conversationRepository.loadNextPage(cursor = null)
|
_isRefreshing.value = false
|
||||||
if (result is Result.Success) {
|
|
||||||
nextCursor = result.data
|
|
||||||
_hasMore.value = result.data != null
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.w(e) { "Failed to refresh conversations" }
|
|
||||||
} finally {
|
|
||||||
_isRefreshing.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,19 +253,23 @@ class NavHostViewModel(
|
||||||
|
|
||||||
fun onSearchQueryChanged(query: String) {
|
fun onSearchQueryChanged(query: String) {
|
||||||
_searchQuery.value = query
|
_searchQuery.value = query
|
||||||
searchJob?.cancel()
|
|
||||||
|
|
||||||
if (query.isBlank()) {
|
if (query.isBlank()) {
|
||||||
_groupedConversations.value = groupConversationsByDate(_recentConversations.value)
|
_groupedConversations.value = groupConversationsByDate(_recentConversations.value)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
searchJob = viewModelScope.launch {
|
@OptIn(FlowPreview::class)
|
||||||
delay(300)
|
private fun observeSearchQuery() {
|
||||||
val filtered = _recentConversations.value.filter { conversation ->
|
viewModelScope.launch {
|
||||||
conversation.title?.contains(query, ignoreCase = true) == true
|
_searchQuery
|
||||||
}
|
.filter { it.isNotBlank() }
|
||||||
_groupedConversations.value = groupConversationsByDate(filtered)
|
.debounce(SEARCH_DEBOUNCE_MS)
|
||||||
|
.collectLatest { query ->
|
||||||
|
val filtered = _recentConversations.value.filter { conversation ->
|
||||||
|
conversation.title?.contains(query, ignoreCase = true) == true
|
||||||
|
}
|
||||||
|
_groupedConversations.value = groupConversationsByDate(filtered)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -312,8 +311,16 @@ class NavHostViewModel(
|
||||||
if (result is Result.Success) {
|
if (result is Result.Success) {
|
||||||
val now = Clock.System.now()
|
val now = Clock.System.now()
|
||||||
_banners.value = result.data.filter { banner ->
|
_banners.value = result.data.filter { banner ->
|
||||||
val from = banner.displayFrom?.let { runCatching { Instant.parse(it) }.getOrNull() }
|
val from = banner.displayFrom?.let {
|
||||||
val to = banner.displayTo?.let { runCatching { Instant.parse(it) }.getOrNull() }
|
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 afterStart = from == null || now >= from
|
||||||
val beforeEnd = to == null || now < to
|
val beforeEnd = to == null || now < to
|
||||||
afterStart && beforeEnd
|
afterStart && beforeEnd
|
||||||
|
|
@ -332,24 +339,29 @@ class NavHostViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissBanner(bannerId: String) {
|
fun dismissBanner(bannerId: String) {
|
||||||
_dismissedBannerIds.value = _dismissedBannerIds.value + bannerId
|
_dismissedBannerIds.update { it + bannerId }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkBackendVersion() {
|
private fun checkBackendVersion() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = configRepository.checkBackendVersion()
|
when (val result = configRepository.checkBackendVersion()) {
|
||||||
if (result is Result.Success) {
|
is Result.Success -> {
|
||||||
val checkResult = result.data
|
val checkResult = result.data
|
||||||
val detectedVersion = checkResult.backendVersion
|
val detectedVersion = checkResult.backendVersion
|
||||||
if (!checkResult.isCompatible && detectedVersion != null) {
|
if (!checkResult.isCompatible && detectedVersion != null) {
|
||||||
val dismissedVersion = settingsDataStore.dismissedVersionWarning.first()
|
val dismissedVersion = settingsDataStore.dismissedVersionWarning.first()
|
||||||
if (dismissedVersion != detectedVersion) {
|
if (dismissedVersion != detectedVersion) {
|
||||||
_versionMismatch.value = VersionMismatchState(
|
_versionMismatch.value = VersionMismatchState(
|
||||||
supportedVersion = checkResult.supportedVersion,
|
supportedVersion = checkResult.supportedVersion,
|
||||||
backendVersion = detectedVersion,
|
backendVersion = detectedVersion,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is Result.Error -> {
|
||||||
|
Logger.w(result.exception) { "Failed to check backend version: ${result.message}" }
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue