From 739ff4f62bc7f7a44639c97a293a032341540a9a Mon Sep 17 00:00:00 2001
From: M57 <41645758+Hy4ri@users.noreply.github.com>
Date: Wed, 8 Jul 2026 01:00:56 +0300
Subject: [PATCH] fix(chat): surface all disconnected states in reconnect
banner (#491) (#493)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(chat): surface all disconnected states in reconnect banner (issue #491)
The connection-status banner only appeared for RECONNECTING/DISCONNECTED,
so NO_NETWORK and AUTH_EXPIRED left the user staring at a silent chat with
no feedback and no way to recover — the core of the 'reconnecting is not
user-friendly' complaint.
ChatTopBanner now shows for every non-connected, non-connecting state:
- RECONNECTING: spinner + 'Reconnecting to Hermes…' (auto, no button)
- DISCONNECTED: 'Disconnected from Hermes' + Reconnect
- NO_NETWORK: 'No network connection' + Reconnect (force a retry)
- AUTH_EXPIRED: 'Session expired' + 'Sign in again' -> AuthLoginScreen
The auto-reconnect engine (backoff, network-return trigger, in-flight
message queue in HermesWsClient) is untouched — this only makes the stuck
states visible and actionable.
* fix(chat): use LocalHermesStatusColors for reconnect banner (review fix)
Per AGENTS.md, semantic status colors must come from
LocalHermesStatusColors, never MaterialTheme.colorScheme. The reconnect
banner used errorContainer/onErrorContainer directly, which is both a
semantic-color violation and inconsistent with the rest of the app
(Navigation.kt maps DISCONNECTED/NO_NETWORK/AUTH_EXPIRED -> .error;
TelegramOnboardingDialog uses .errorContainer for error surfaces).
Switch Surface bg to errorContainer and foreground (spinner, text,
button content) to error via LocalHermesStatusColors.current.
---
.../m57/hermescontrol/ui/chat/ChatScreen.kt | 78 +++++++++++++------
app/src/main/res/values/strings.xml | 3 +
2 files changed, 58 insertions(+), 23 deletions(-)
diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt
index 13a0f725..1aa98405 100644
--- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt
+++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt
@@ -109,12 +109,14 @@ import androidx.core.content.FileProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
+import com.m57.hermescontrol.AuthLoginScreen
import com.m57.hermescontrol.NavigationController
import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.model.Attachment
import com.m57.hermescontrol.data.ws.CommandCatalog
import com.m57.hermescontrol.data.ws.ConnectionStatus
import com.m57.hermescontrol.notification.NotificationHelper
+import com.m57.hermescontrol.theme.LocalHermesStatusColors
import com.m57.hermescontrol.theme.StatusRed
import com.m57.hermescontrol.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.HermesScaffold
@@ -360,6 +362,7 @@ fun ChatScreen(
ChatTopBanner(
connectionStatus = state.connectionStatus,
onReconnect = viewModel::reconnect,
+ onAuthExpired = { NavigationController.navigateTo(AuthLoginScreen) },
)
Box(
@@ -1337,18 +1340,20 @@ private fun ChatLifecycleEffects(
private fun ChatTopBanner(
connectionStatus: ConnectionStatus,
onReconnect: () -> Unit,
+ onAuthExpired: () -> Unit,
) {
+ val isShown =
+ connectionStatus != ConnectionStatus.CONNECTED &&
+ connectionStatus != ConnectionStatus.CONNECTING
androidx.compose.animation.AnimatedVisibility(
- visible =
- connectionStatus == ConnectionStatus.RECONNECTING ||
- connectionStatus == ConnectionStatus.DISCONNECTED,
+ visible = isShown,
enter = androidx.compose.animation.expandVertically() + androidx.compose.animation.fadeIn(),
exit = androidx.compose.animation.shrinkVertically() + androidx.compose.animation.fadeOut(),
) {
androidx.compose.material3.Surface(
modifier = Modifier.fillMaxWidth(),
- color = MaterialTheme.colorScheme.errorContainer,
- contentColor = MaterialTheme.colorScheme.onErrorContainer,
+ color = LocalHermesStatusColors.current.errorContainer,
+ contentColor = LocalHermesStatusColors.current.error,
) {
Row(
modifier =
@@ -1358,25 +1363,52 @@ private fun ChatTopBanner(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
- Text(
- text =
- if (connectionStatus == ConnectionStatus.RECONNECTING) {
- stringResource(R.string.chat_status_reconnecting)
- } else {
- stringResource(R.string.chat_status_disconnected)
- },
- style = MaterialTheme.typography.bodyMedium,
- )
- if (connectionStatus == ConnectionStatus.DISCONNECTED) {
- TextButton(
- onClick = onReconnect,
- colors =
- androidx.compose.material3.ButtonDefaults.textButtonColors(
- contentColor = MaterialTheme.colorScheme.onErrorContainer,
- ),
- ) {
- Text(stringResource(R.string.chat_action_reconnect))
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ if (connectionStatus == ConnectionStatus.RECONNECTING) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(16.dp).padding(end = 8.dp),
+ strokeWidth = 2.dp,
+ color = LocalHermesStatusColors.current.error,
+ )
}
+ Text(
+ text =
+ when (connectionStatus) {
+ ConnectionStatus.RECONNECTING -> stringResource(R.string.chat_status_reconnecting)
+ ConnectionStatus.DISCONNECTED -> stringResource(R.string.chat_status_disconnected)
+ ConnectionStatus.NO_NETWORK -> stringResource(R.string.chat_status_no_network)
+ ConnectionStatus.AUTH_EXPIRED -> stringResource(R.string.chat_status_auth_expired)
+ else -> ""
+ },
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+ when (connectionStatus) {
+ ConnectionStatus.DISCONNECTED,
+ ConnectionStatus.NO_NETWORK,
+ -> {
+ TextButton(
+ onClick = onReconnect,
+ colors =
+ androidx.compose.material3.ButtonDefaults.textButtonColors(
+ contentColor = LocalHermesStatusColors.current.error,
+ ),
+ ) {
+ Text(stringResource(R.string.chat_action_reconnect))
+ }
+ }
+ ConnectionStatus.AUTH_EXPIRED -> {
+ TextButton(
+ onClick = onAuthExpired,
+ colors =
+ androidx.compose.material3.ButtonDefaults.textButtonColors(
+ contentColor = LocalHermesStatusColors.current.error,
+ ),
+ ) {
+ Text(stringResource(R.string.chat_action_sign_in))
+ }
+ }
+ else -> { /* RECONNECTING: auto, no button */ }
}
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 936d1307..dc0d12b5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -82,11 +82,14 @@
Search
Close search
Reconnect
+ Sign in again
Ready to chat
Send a message to start a conversation
Connecting to Hermes\u2026
Reconnecting to Hermes\u2026
Disconnected from Hermes
+ No network connection
+ Session expired \u2014 please sign in again
Thinking\u2026
Thinking: %1$s\u2026
Not connected\u2026