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