From 61dc82d17ce8a946d86753556271e9aa15fb7579 Mon Sep 17 00:00:00 2001 From: cogwheel <172976095+cogwheel0@users.noreply.github.com> Date: Sat, 20 Dec 2025 14:02:27 +0530 Subject: [PATCH] feat(chat): optimize performance and focus handling in chat UI --- lib/features/chat/views/chat_page.dart | 19 +++++------- .../chat/widgets/modern_chat_input.dart | 30 ++++++++++++------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 23381ab..d2148f4 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -1570,16 +1570,14 @@ class _ChatPageState extends ConsumerState { height: 1.3, ); - // Keyboard visibility - final keyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0; + // Keyboard visibility - use viewInsetsOf for more efficient partial subscription + final keyboardVisible = MediaQuery.viewInsetsOf(context).bottom > 0; // Whether the messages list can actually scroll (avoids showing button when not needed) final canScroll = _scrollController.hasClients && _scrollController.position.maxScrollExtent > 0; - // Check if any message is currently streaming (for scroll button indicator) - final isStreamingAnyMessage = ref - .watch(chatMessagesProvider) - .any((msg) => msg.isStreaming); + // Use dedicated streaming provider to avoid iterating all messages on rebuild + final isStreamingAnyMessage = ref.watch(isChatStreamingProvider); // On keyboard open, if already near bottom, auto-scroll to bottom to keep input visible if (keyboardVisible && !_lastKeyboardVisible) { @@ -1601,15 +1599,12 @@ class _ChatPageState extends ConsumerState { }); } - // Focus composer on app startup once + // Focus composer on app startup once (minimal delay for layout to settle) if (!_didStartupFocus) { _didStartupFocus = true; WidgetsBinding.instance.addPostFrameCallback((_) { - Future.delayed(const Duration(milliseconds: 200), () { - if (!mounted) return; - final current = ref.read(inputFocusTriggerProvider); - ref.read(inputFocusTriggerProvider.notifier).set(current + 1); - }); + if (!mounted) return; + ref.read(inputFocusTriggerProvider.notifier).increment(); }); } diff --git a/lib/features/chat/widgets/modern_chat_input.dart b/lib/features/chat/widgets/modern_chat_input.dart index 5dee4c3..c588c7c 100644 --- a/lib/features/chat/widgets/modern_chat_input.dart +++ b/lib/features/chat/widgets/modern_chat_input.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import '../../../shared/theme/theme_extensions.dart'; // app_theme not required here; using theme extension tokens @@ -183,13 +184,23 @@ class _ModernChatInputState extends ConsumerState } _pendingFocus = true; - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) return; + // Request focus synchronously if we're already in a safe context, + // otherwise defer to next frame + if (WidgetsBinding.instance.schedulerPhase == + SchedulerPhase.persistentCallbacks) { + // We're in a build/layout phase, defer to next frame + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _pendingFocus = false; + if (widget.enabled && !_focusNode.hasFocus) { + _focusNode.requestFocus(); + } + }); + } else { + // Safe to request focus immediately _pendingFocus = false; - if (widget.enabled && !_focusNode.hasFocus) { - _focusNode.requestFocus(); - } - }); + _focusNode.requestFocus(); + } } @override @@ -1032,11 +1043,8 @@ class _ModernChatInputState extends ConsumerState }); }); - final messages = ref.watch(chatMessagesProvider); - final isGenerating = - messages.isNotEmpty && - messages.last.role == 'assistant' && - messages.last.isStreaming; + // Use dedicated streaming provider to avoid rebuilding on every message change + final isGenerating = ref.watch(isChatStreamingProvider); final stopGeneration = ref.read(stopGenerationProvider); final webSearchEnabled = ref.watch(webSearchEnabledProvider);