Put a refused delete on the session it was refused for
Deleting a session the server no longer has replaced the whole list with an error. The server answered -- it said "no session <id>" -- so what it told us is about one row, and every other row was still exactly as fetched. The list going away said otherwise. The line between the two scopes is whether the server answered. Answered and refused is about one item and belongs on that item; never answered leaves every row stale against a server that has stopped talking, and that is the list's own state to report. Both cases exist here and had been sharing one slot, which is why the message had to hedge about which it was. So failures acting on one session live in a map keyed by its id and render inside its card. The path out is the next successful load, which clears the map: an entry would otherwise outlive the session it names and reappear against whatever the phone fetched next. The shape is dev-updater's `cardStates`, which solved this first; the rule above is theirs too, and it moves their "gave up waiting" case the other way, to list-level, which is a good sign it is a rule rather than a description of what either of us already had. Verified on the emulator by producing the real failure -- delete a session out from under the app with curl, then delete its stale row from the phone. The refusal appears on that card, the other card is untouched, the list stays, and Refresh clears it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
1 parent
9e11e860e5
commit
7a02e79613
1 file changed
+30
-2
@@ -57,11 +57,25 @@ fun SessionListScreen(
|
|||||||
var listState by remember { mutableStateOf<LoadState<List<SessionSummary>>>(LoadState.Loading) }
|
var listState by remember { mutableStateOf<LoadState<List<SessionSummary>>>(LoadState.Loading) }
|
||||||
var confirmingDelete by remember { mutableStateOf<SessionSummary?>(null) }
|
var confirmingDelete by remember { mutableStateOf<SessionSummary?>(null) }
|
||||||
|
|
||||||
|
// Failures that belong to one session rather than to the list, keyed by
|
||||||
|
// its id and shown on its own card. The two scopes are decided by
|
||||||
|
// whether the server answered: it answered and refused, so this says
|
||||||
|
// nothing about the other rows, where a server that has stopped
|
||||||
|
// answering leaves every row stale and is `listState`'s to report.
|
||||||
|
//
|
||||||
|
// Cleared on the next successful load below -- an entry outlives its
|
||||||
|
// session otherwise, and would reappear against whatever the phone
|
||||||
|
// fetched next.
|
||||||
|
var deleteErrors by remember { mutableStateOf<Map<String, String>>(emptyMap()) }
|
||||||
|
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
listState = LoadState.Loading
|
listState = LoadState.Loading
|
||||||
scope.launch {
|
scope.launch {
|
||||||
listState = try {
|
listState = try {
|
||||||
withContext(Dispatchers.IO) { LoadState.Loaded(fetchSessions(settings)) }
|
val loaded =
|
||||||
|
withContext(Dispatchers.IO) { LoadState.Loaded(fetchSessions(settings)) }
|
||||||
|
deleteErrors = emptyMap()
|
||||||
|
loaded
|
||||||
} catch (e: ApiException) {
|
} catch (e: ApiException) {
|
||||||
LoadState.failed(e)
|
LoadState.failed(e)
|
||||||
}
|
}
|
||||||
@@ -114,6 +128,7 @@ fun SessionListScreen(
|
|||||||
items(ordered, key = { it.id }) { session ->
|
items(ordered, key = { it.id }) { session ->
|
||||||
SessionCard(
|
SessionCard(
|
||||||
session = session,
|
session = session,
|
||||||
|
error = deleteErrors[session.id],
|
||||||
onOpen = { onOpen(session) },
|
onOpen = { onOpen(session) },
|
||||||
onLongPress = { confirmingDelete = session },
|
onLongPress = { confirmingDelete = session },
|
||||||
)
|
)
|
||||||
@@ -143,7 +158,8 @@ fun SessionListScreen(
|
|||||||
withContext(Dispatchers.IO) { deleteSession(settings, session.id) }
|
withContext(Dispatchers.IO) { deleteSession(settings, session.id) }
|
||||||
refresh()
|
refresh()
|
||||||
} catch (e: ApiException) {
|
} catch (e: ApiException) {
|
||||||
listState = LoadState.Error(e.message ?: "Delete failed")
|
deleteErrors =
|
||||||
|
deleteErrors + (session.id to (e.message ?: "Delete failed"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) { Text("Delete") }
|
}) { Text("Delete") }
|
||||||
@@ -159,6 +175,8 @@ fun SessionListScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun SessionCard(
|
private fun SessionCard(
|
||||||
session: SessionSummary,
|
session: SessionSummary,
|
||||||
|
/** What went wrong acting on *this* session, if anything has. */
|
||||||
|
error: String?,
|
||||||
onOpen: () -> Unit,
|
onOpen: () -> Unit,
|
||||||
onLongPress: () -> Unit,
|
onLongPress: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -194,6 +212,16 @@ private fun SessionCard(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
error?.let {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
// The server's own words, unprefixed, the way every other
|
||||||
|
// failure in this app is shown.
|
||||||
|
Text(
|
||||||
|
it,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user