Merge pull request #23 from garfiec/refactor/nav-host-viewmodel_stage5

refactor: NavHostViewModel stage 5 — extract drawer models and display mapping
This commit is contained in:
Garfie Chiu 2026-04-07 21:23:04 -05:00 committed by GitHub
commit 56fd4842be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 84 additions and 80 deletions

View file

@ -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<Pair<String, List<Conversation>>>,
val activeId: String?,
val favIds: Set<String>,
val favConvos: List<Conversation>,
val query: String,
)
internal fun Conversation.toDrawerDisplayData(
activeConversationId: String?,
favoriteIds: Set<String>,
): 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}"
}
}
}

View file

@ -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<Pair<String, List<DrawerConversationDisplayData>>> = emptyList(),
val favoriteConversations: List<DrawerConversationDisplayData> = emptyList(),
val searchQuery: String = "",
val isRefreshing: Boolean = false,
val isLoadingMore: Boolean = false,
val hasMore: Boolean = true,
)

View file

@ -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<Pair<String, List<DrawerConversationDisplayData>>> = emptyList(),
val favoriteConversations: List<DrawerConversationDisplayData> = 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<Pair<String, List<Conversation>>>,
val activeId: String?,
val favIds: Set<String>,
val favConvos: List<Conversation>,
val query: String,
)
private fun Conversation.toDrawerDisplayData(
activeConversationId: String?,
favoriteIds: Set<String>,
): 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}"
}
}
}