* fix(#219): add pagination to sessions history tab The /api/sessions endpoint was called without pagination params, so the server returned only its default limit of 20 sessions. - SessionListResponse: added total/limit/offset fields from server response - HermesApiService: added @Query params (limit, offset, order) to getSessions() - SessionsViewModel: added loadMore() that appends pages, hasMore derived from total - SessionsScreen: added 'Load more sessions' button + loading spinner at bottom of list * chore: remove plan file from git * fix: compilation errors in sessions pagination * chore: gitignore plan files * fix: remove duplicate brace --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
067f4500aa
commit
4ba74c2efa
7 files changed
+114
-7
No files matched your search
@@ -2,6 +2,9 @@ package com.m57.hermescontrol.data.model
|
||||
|
||||
data class SessionListResponse(
|
||||
val sessions: List<SessionInfo>,
|
||||
val total: Int = 0,
|
||||
val limit: Int = 0,
|
||||
val offset: Int = 0,
|
||||
)
|
||||
|
||||
data class SessionInfo(
|
||||
|
||||
@@ -67,7 +67,11 @@ interface HermesApiService {
|
||||
suspend fun getStatus(): Response<StatusResponse>
|
||||
|
||||
@GET("api/sessions")
|
||||
suspend fun getSessions(): Response<SessionListResponse>
|
||||
suspend fun getSessions(
|
||||
@Query("limit") limit: Int = 20,
|
||||
@Query("offset") offset: Int = 0,
|
||||
@Query("order") order: String = "recent",
|
||||
): Response<SessionListResponse>
|
||||
|
||||
@GET("api/sessions/{id}/messages")
|
||||
suspend fun getSessionMessages(
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package com.m57.hermescontrol.ui.sessions
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -14,6 +16,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.History
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -28,6 +31,7 @@ import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.m57.hermescontrol.ChatScreen
|
||||
@@ -156,6 +160,38 @@ fun SessionsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load more button
|
||||
if (state.hasMore || state.isLoadingMore) {
|
||||
item {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = spacing.sm),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (state.isLoadingMore) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "Load more sessions...",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier =
|
||||
Modifier
|
||||
.testTag("load_more_sessions")
|
||||
.clickable(role = Role.Button) {
|
||||
viewModel.loadMore()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.m57.hermescontrol.ui.sessions
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.m57.hermescontrol.data.model.SessionInfo
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||
@@ -9,26 +10,47 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class SessionsUiState(
|
||||
val isLoading: Boolean = false,
|
||||
val isLoadingMore: Boolean = false,
|
||||
val sessions: List<SessionInfo> = emptyList(),
|
||||
val total: Int = 0,
|
||||
val errorMessage: String? = null,
|
||||
)
|
||||
) {
|
||||
val hasMore: Boolean get() = total > sessions.size
|
||||
}
|
||||
|
||||
class SessionsViewModel : ViewModel() {
|
||||
private val _uiState = MutableStateFlow(SessionsUiState())
|
||||
val uiState: StateFlow<SessionsUiState> = _uiState.asStateFlow()
|
||||
|
||||
/** Page size sent to the server — matches the default the gateway uses. */
|
||||
private companion object {
|
||||
const val PAGE_SIZE = 20
|
||||
}
|
||||
|
||||
/** Load (or reload) sessions from page 0. Used by pull-to-refresh and initial load. */
|
||||
fun loadSessions() {
|
||||
safeLaunchLoad(
|
||||
apiCall = { safeApiCall { ApiClient.hermesApi.getSessions() } },
|
||||
apiCall = {
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.getSessions(
|
||||
limit = PAGE_SIZE,
|
||||
offset = 0,
|
||||
order = "recent",
|
||||
)
|
||||
}
|
||||
},
|
||||
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||
onSuccess = { data ->
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
isLoadingMore = false,
|
||||
sessions = data?.sessions.orEmpty(),
|
||||
total = data?.total ?: 0,
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -36,10 +58,49 @@ class SessionsViewModel : ViewModel() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
isLoadingMore = false,
|
||||
errorMessage = "Failed to load sessions: $errorMsg",
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** Load the next page and append to the existing session list. */
|
||||
fun loadMore() {
|
||||
val state = _uiState.value
|
||||
if (state.isLoadingMore || !state.hasMore) return
|
||||
|
||||
_uiState.update { it.copy(isLoadingMore = true) }
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.getSessions(
|
||||
limit = PAGE_SIZE,
|
||||
offset = state.sessions.size,
|
||||
order = "recent",
|
||||
)
|
||||
}
|
||||
when (result) {
|
||||
is com.m57.hermescontrol.data.remote.NetworkResult.Success -> {
|
||||
val data = result.data
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoadingMore = false,
|
||||
sessions = it.sessions + data.sessions,
|
||||
total = data.total,
|
||||
)
|
||||
}
|
||||
}
|
||||
is com.m57.hermescontrol.data.remote.NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoadingMore = false,
|
||||
errorMessage = "Failed to load more: ${result.error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user