From cc379c1828fdfda951324fa1e1daad52caece280 Mon Sep 17 00:00:00 2001 From: Avinal Kumar Date: Thu, 21 May 2026 17:31:36 +0530 Subject: [PATCH] Add loading indicator, error display, archived memos API Loading state: - CircularProgressIndicator shown during first fetch instead of "no memos yet". isInitialLoading flag in MemoListUiState cleared after first emission from observeMemos(). Error display: - Failed refresh/load errors shown as red text in memo list - Previously errors were captured but never displayed to user Archived memos: - listArchivedMemos() API endpoint added (filter: state == ARCHIVED) - "view archived memos" link in explorer page (placeholder for full UI) 102 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Avinal Kumar --- .../com/avinal/memos/api/MemosApiClient.kt | 7 +++++++ .../com/avinal/memos/ui/memos/MainScreen.kt | 8 ++++++++ .../com/avinal/memos/ui/memos/MemoListScreen.kt | 17 ++++++++++++++++- .../avinal/memos/ui/memos/MemoListViewModel.kt | 9 +++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/api/MemosApiClient.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/api/MemosApiClient.kt index 9058ed9..c711086 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/api/MemosApiClient.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/api/MemosApiClient.kt @@ -138,6 +138,13 @@ class MemosApiClient( }.body() } + suspend fun listArchivedMemos(): ApiResult = apiCall { + httpClient.get(url("/memos")) { + parameter("pageSize", 50) + parameter("filter", "state == \"ARCHIVED\"") + }.body() + } + suspend fun deleteMemo(id: String): ApiResult = apiCall { val response = httpClient.delete(url("/memos/$id")) if (!response.status.isSuccess()) { diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MainScreen.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MainScreen.kt index 3b026df..4ad6342 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MainScreen.kt @@ -247,6 +247,14 @@ private fun ExplorerPage( ) } + Spacer(Modifier.height(12.dp)) + Text( + "view archived memos", + fontSize = 14.sp, + color = accent, + modifier = Modifier.clickable { /* navigate to archived */ }.padding(vertical = 4.dp), + ) + Spacer(Modifier.height(16.dp)) Row( diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListScreen.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListScreen.kt index c6735d3..09205cd 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListScreen.kt @@ -349,7 +349,13 @@ fun MemoListScreen( ) } - if (memos.isEmpty() && !uiState.isRefreshing) { + if (uiState.isInitialLoading && memos.isEmpty()) { + item { + Box(Modifier.fillMaxWidth().padding(top = 48.dp), contentAlignment = Alignment.Center) { + androidx.compose.material3.CircularProgressIndicator(color = accent, strokeWidth = 2.dp) + } + } + } else if (memos.isEmpty() && !uiState.isRefreshing) { item { Box(Modifier.fillMaxWidth().padding(top = 48.dp), contentAlignment = Alignment.Center) { Text("no memos yet", fontSize = 15.sp, color = subtleColor) @@ -357,6 +363,15 @@ fun MemoListScreen( } } + if (uiState.error != null) { + item { + Text( + uiState.error!!, fontSize = 12.sp, color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(start = 24.dp, end = 12.dp, top = 4.dp, bottom = 4.dp), + ) + } + } + items(memos, key = { it.id }) { memo -> MemoCard( memo = memo, diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListViewModel.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListViewModel.kt index aeb9ff2..be775c7 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListViewModel.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -22,6 +23,7 @@ data class MemoListUiState( val searchQuery: String = "", val isSearching: Boolean = false, val error: String? = null, + val isInitialLoading: Boolean = true, ) class MemoListViewModel(private val memoRepository: MemoRepository) : ViewModel() { @@ -32,6 +34,13 @@ class MemoListViewModel(private val memoRepository: MemoRepository) : ViewModel( private val _searchQuery = MutableStateFlow("") private var searchJob: Job? = null + init { + viewModelScope.launch { + memoRepository.observeMemos().first() + _uiState.update { it.copy(isInitialLoading = false) } + } + } + val memos: StateFlow> = combine( memoRepository.observeMemos(), _searchQuery,