From 586f463c9c0092f7ba8cb9d1f1c5178ded9603c9 Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:06:06 +0300 Subject: [PATCH] Fix performance issue by caching typing effect settings in ChatUiState (#329) * Fix performance issue by caching typing effect settings in ChatUiState - Moved `AuthManager.isTypingEffectEnabled()` and `AuthManager.getTypingEffectDelayMs()` out of `ChatScreen.kt` recomposition loops. - Stored `typingEffectEnabled` and `typingEffectDelayMs` in `ChatUiState`. - Added `refreshSettings()` to `ChatViewModel` to fetch settings upon initialization and on returning to the chat screen (ON_START). - Prevents expensive EncryptedSharedPreferences XML decryption occurring on every message per token stream update. Fixes #269 * Fix performance issue by caching typing effect settings in ChatUiState - Moved `AuthManager.isTypingEffectEnabled()` and `AuthManager.getTypingEffectDelayMs()` out of `ChatScreen.kt` recomposition loops. - Stored `typingEffectEnabled` and `typingEffectDelayMs` in `ChatUiState`. - Added `refreshSettings()` to `ChatViewModel` to fetch settings upon initialization and on returning to the chat screen (ON_START). - Prevents expensive EncryptedSharedPreferences XML decryption occurring on every message per token stream update. - Fixed `ChatViewModelTest` missing mocks for `AuthManager.isTypingEffectEnabled()` and `AuthManager.getTypingEffectDelayMs()`. - Fixed ktlint unused import and max line length violations. Fixes #269 --- .../m57/hermescontrol/ui/chat/ChatScreen.kt | 19 +++++++++---------- .../hermescontrol/ui/chat/ChatViewModel.kt | 13 +++++++++++++ .../ui/chat/ChatViewModelTest.kt | 2 ++ 3 files changed, 24 insertions(+), 10 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 c0a9a245..7c76affb 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 @@ -104,7 +104,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.m57.hermescontrol.NavigationController import com.m57.hermescontrol.R -import com.m57.hermescontrol.data.local.AuthManager import com.m57.hermescontrol.data.ws.ConnectionStatus import com.m57.hermescontrol.notification.NotificationHelper import com.m57.hermescontrol.theme.StatusRed @@ -174,6 +173,7 @@ fun ChatScreen( androidx.lifecycle.Lifecycle.Event.ON_START -> { NotificationHelper.setAppForeground(context, true) NotificationHelper.stop(context) + viewModel.refreshSettings() viewModel.refreshCurrentSession() } @@ -441,13 +441,15 @@ fun ChatScreen( val isLastMessage = index == state.messages.lastIndex val isAssistant = message.role == MessageRole.ASSISTANT - val typingEnabled = AuthManager.isTypingEffectEnabled() - val typingDelayMs = AuthManager.getTypingEffectDelayMs() - if (typingEnabled && isLastMessage && isAssistant && lastAnimatedMessageId != message.id) { + if (state.typingEffectEnabled && + isLastMessage && + isAssistant && + lastAnimatedMessageId != message.id + ) { StreamingBubbleWithTypingEffect( streaming = message, - typingDelayMs = typingDelayMs, + typingDelayMs = state.typingEffectDelayMs, isDark = isDark, onAnimationComplete = { lastAnimatedMessageId = message.id @@ -466,13 +468,10 @@ fun ChatScreen( // Streaming message — rendered separately for O(1) updates state.streamingMessage?.let { streaming -> item(key = "streaming-${streaming.id}") { - val typingEnabled = AuthManager.isTypingEffectEnabled() - val typingDelayMs = AuthManager.getTypingEffectDelayMs() - - if (typingEnabled && streaming.isStreaming) { + if (state.typingEffectEnabled && streaming.isStreaming) { StreamingBubbleWithTypingEffect( streaming = streaming, - typingDelayMs = typingDelayMs, + typingDelayMs = state.typingEffectDelayMs, isDark = isDark, ) } else { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt index fedb1bcd..ecd49836 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatViewModel.kt @@ -57,6 +57,9 @@ data class ChatUiState( val searchQuery: String = "", val searchMatchIndices: List = emptyList(), val currentSearchMatchIndex: Int = -1, + // Cached settings + val typingEffectEnabled: Boolean = true, + val typingEffectDelayMs: Int = 30, ) { /** Convenience — derived from [connectionStatus]. */ val isConnected: Boolean get() = connectionStatus == ConnectionStatus.CONNECTED @@ -118,6 +121,7 @@ class ChatViewModel( ) init { + refreshSettings() connectWebSocket() viewModelScope.launch { wsClient.events.collect { event -> @@ -860,6 +864,15 @@ class ChatViewModel( loadSessionMessages(sessionId) } + fun refreshSettings() { + _uiState.update { state -> + state.copy( + typingEffectEnabled = AuthManager.isTypingEffectEnabled(), + typingEffectDelayMs = AuthManager.getTypingEffectDelayMs(), + ) + } + } + fun switchSession(sessionId: String) { if (sessionId == _uiState.value.currentSessionId) return diff --git a/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt index fc311cf8..3c070465 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt @@ -64,6 +64,8 @@ class ChatViewModelTest { mockConnectionStatus.value = ConnectionStatus.DISCONNECTED every { AuthManager.getToken() } returns "test-token" + every { AuthManager.isTypingEffectEnabled() } returns true + every { AuthManager.getTypingEffectDelayMs() } returns 30 every { HermesWsClient.events } returns mockEventsFlow every { HermesWsClient.connectionStatus } returns mockConnectionStatus every { HermesWsClient.connect() } returns Unit