1
0
mirror of https://github.com/avinal/nikki.git synced 2026-07-03 21:40:09 +05:30

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 <avinal.xlvii@gmail.com>
This commit is contained in:
2026-05-21 17:31:36 +05:30
parent 25a5c51b0a
commit cc379c1828
4 changed files with 40 additions and 1 deletions
@@ -138,6 +138,13 @@ class MemosApiClient(
}.body()
}
suspend fun listArchivedMemos(): ApiResult<ListMemosResponse> = apiCall {
httpClient.get(url("/memos")) {
parameter("pageSize", 50)
parameter("filter", "state == \"ARCHIVED\"")
}.body()
}
suspend fun deleteMemo(id: String): ApiResult<Unit> = apiCall {
val response = httpClient.delete(url("/memos/$id"))
if (!response.status.isSuccess()) {
@@ -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(
@@ -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,
@@ -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<List<Memo>> = combine(
memoRepository.observeMemos(),
_searchQuery,