From ebcae8478e0b3007622147f437de741afb215574 Mon Sep 17 00:00:00 2001 From: Garfie Date: Tue, 7 Apr 2026 20:45:44 -0500 Subject: [PATCH] refactor: NavHostViewModel stage 5 - extract drawer models and display mapping --- .../shared/navigation/DrawerDisplayMapping.kt | 52 ++++++++++++ .../shared/navigation/DrawerUiModels.kt | 32 ++++++++ .../shared/navigation/NavHostViewModel.kt | 80 ------------------- 3 files changed, 84 insertions(+), 80 deletions(-) create mode 100644 shared/src/commonMain/kotlin/com/garfiec/librechat/shared/navigation/DrawerDisplayMapping.kt create mode 100644 shared/src/commonMain/kotlin/com/garfiec/librechat/shared/navigation/DrawerUiModels.kt diff --git a/shared/src/commonMain/kotlin/com/garfiec/librechat/shared/navigation/DrawerDisplayMapping.kt b/shared/src/commonMain/kotlin/com/garfiec/librechat/shared/navigation/DrawerDisplayMapping.kt new file mode 100644 index 0000000..643c470 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/garfiec/librechat/shared/navigation/DrawerDisplayMapping.kt @@ -0,0 +1,52 @@ +package com.garfiec.librechat.shared.navigation + +import com.garfiec.librechat.core.common.extensions.toInstantOrNull +import com.garfiec.librechat.core.common.extensions.formatMonthAbbrev +import com.garfiec.librechat.core.model.Conversation +import kotlin.time.Clock +import kotlin.time.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime + +internal data class DrawerDataSnapshot( + val grouped: List>>, + val activeId: String?, + val favIds: Set, + val favConvos: List, + val query: String, +) + +internal fun Conversation.toDrawerDisplayData( + activeConversationId: String?, + favoriteIds: Set, +): DrawerConversationDisplayData { + val convId = conversationId ?: "" + return DrawerConversationDisplayData( + conversationId = convId, + title = title ?: "New Chat", + model = model, + endpoint = endpoint, + relativeTime = updatedAt?.toInstantOrNull()?.toRelativeTimeString() ?: "", + isActive = convId == activeConversationId, + isFavorite = convId in favoriteIds, + ) +} + +internal fun Instant.toRelativeTimeString(): String { + val now = Clock.System.now() + val duration = now - this + val minutes = duration.inWholeMinutes + val hours = duration.inWholeHours + val days = duration.inWholeDays + + return when { + minutes < 1 -> "Just now" + minutes < 60 -> "${minutes}m ago" + hours < 24 -> "${hours}h ago" + days < 7 -> "${days}d ago" + else -> { + val date = toLocalDateTime(TimeZone.currentSystemDefault()).date + "${formatMonthAbbrev(date.monthNumber)} ${date.dayOfMonth}" + } + } +} diff --git a/shared/src/commonMain/kotlin/com/garfiec/librechat/shared/navigation/DrawerUiModels.kt b/shared/src/commonMain/kotlin/com/garfiec/librechat/shared/navigation/DrawerUiModels.kt new file mode 100644 index 0000000..d7779e7 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/garfiec/librechat/shared/navigation/DrawerUiModels.kt @@ -0,0 +1,32 @@ +package com.garfiec.librechat.shared.navigation + +import androidx.compose.runtime.Immutable +import com.garfiec.librechat.core.model.EModelEndpoint + +/** + * Lightweight snapshot of fields DrawerConversationItem actually renders. + * Avoids passing the full 28-field Conversation through composition. + */ +@Immutable +data class DrawerConversationDisplayData( + val conversationId: String, + val title: String, + val model: String?, + val endpoint: EModelEndpoint?, + val relativeTime: String, + val isActive: Boolean, + val isFavorite: Boolean, +) + +/** + * Combined UI state for the drawer sidebar. + */ +@Immutable +data class DrawerUiState( + val groupedConversations: List>> = emptyList(), + val favoriteConversations: List = emptyList(), + val searchQuery: String = "", + val isRefreshing: Boolean = false, + val isLoadingMore: Boolean = false, + val hasMore: Boolean = true, +) 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 e5e1f07..823c0f9 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 @@ -1,17 +1,12 @@ package com.garfiec.librechat.shared.navigation -import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.garfiec.librechat.core.common.extensions.toInstantOrNull -import com.garfiec.librechat.core.common.extensions.formatMonthAbbrev import com.garfiec.librechat.core.data.repository.AuthRepository import com.garfiec.librechat.core.data.repository.BannerRepository import com.garfiec.librechat.core.data.repository.ConfigRepository import com.garfiec.librechat.core.data.repository.ConversationRepository import com.garfiec.librechat.core.model.Banner -import com.garfiec.librechat.core.model.Conversation -import com.garfiec.librechat.core.model.EModelEndpoint import com.garfiec.librechat.core.network.client.TokenManager import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -23,38 +18,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import co.touchlab.kermit.Logger import com.garfiec.librechat.core.common.extensions.firstBlocking -import kotlin.time.Clock -import kotlin.time.Instant -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime - -/** - * Lightweight snapshot of fields DrawerConversationItem actually renders. - * Avoids passing the full 28-field Conversation through composition. - */ -@Immutable -data class DrawerConversationDisplayData( - val conversationId: String, - val title: String, - val model: String?, - val endpoint: EModelEndpoint?, - val relativeTime: String, - val isActive: Boolean, - val isFavorite: Boolean, -) - -/** - * Combined UI state for the drawer sidebar. - */ -@Immutable -data class DrawerUiState( - val groupedConversations: List>> = emptyList(), - val favoriteConversations: List = emptyList(), - val searchQuery: String = "", - val isRefreshing: Boolean = false, - val isLoadingMore: Boolean = false, - val hasMore: Boolean = true, -) class NavHostViewModel( private val authRepository: AuthRepository, @@ -205,46 +168,3 @@ class NavHostViewModel( } } } - -private data class DrawerDataSnapshot( - val grouped: List>>, - val activeId: String?, - val favIds: Set, - val favConvos: List, - val query: String, -) - -private fun Conversation.toDrawerDisplayData( - activeConversationId: String?, - favoriteIds: Set, -): DrawerConversationDisplayData { - val convId = conversationId ?: "" - return DrawerConversationDisplayData( - conversationId = convId, - title = title ?: "New Chat", - model = model, - endpoint = endpoint, - relativeTime = updatedAt?.toInstantOrNull()?.toRelativeTimeString() ?: "", - isActive = convId == activeConversationId, - isFavorite = convId in favoriteIds, - ) -} - -private fun Instant.toRelativeTimeString(): String { - val now = Clock.System.now() - val duration = now - this - val minutes = duration.inWholeMinutes - val hours = duration.inWholeHours - val days = duration.inWholeDays - - return when { - minutes < 1 -> "Just now" - minutes < 60 -> "${minutes}m ago" - hours < 24 -> "${hours}h ago" - days < 7 -> "${days}d ago" - else -> { - val date = toLocalDateTime(TimeZone.currentSystemDefault()).date - "${formatMonthAbbrev(date.monthNumber)} ${date.dayOfMonth}" - } - } -}