diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index a6ce323..a73cb95 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -69,6 +69,8 @@ class _ChatPageState extends ConsumerState { bool _shouldAutoScrollToBottom = true; bool _autoScrollCallbackScheduled = false; bool _pendingConversationScrollReset = false; + String? _cachedGreetingName; + bool _greetingReady = false; String _formatModelDisplayName(String name, {required bool omitProvider}) { var display = name.trim(); @@ -972,72 +974,71 @@ class _ChatPageState extends ConsumerState { ); final authUser = ref.watch(currentUserProvider2); final user = userFromProfile ?? authUser; - final greetingName = deriveUserDisplayName(user); + String? greetingName; + if (user != null) { + final derived = deriveUserDisplayName(user, fallback: '').trim(); + if (derived.isNotEmpty) { + greetingName = derived; + _cachedGreetingName = derived; + } + } + greetingName ??= _cachedGreetingName; + final hasGreeting = greetingName != null && greetingName.isNotEmpty; + if (hasGreeting && !_greetingReady) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + setState(() { + _greetingReady = true; + }); + }); + } else if (!hasGreeting && _greetingReady) { + _greetingReady = false; + } + final greetingStyle = theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + color: context.conduitTheme.textPrimary, + ); + final greetingHeight = + (greetingStyle?.fontSize ?? 24) * (greetingStyle?.height ?? 1.1); + final String? resolvedGreetingName = hasGreeting ? greetingName : null; + final greetingText = resolvedGreetingName != null + ? l10n.onboardStartTitle(resolvedGreetingName) + : null; return LayoutBuilder( builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.all(Spacing.lg), - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // Minimal, clean empty state - Container( - width: Spacing.xxl + Spacing.xxxl, - height: Spacing.xxl + Spacing.xxxl, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - context.conduitTheme.buttonPrimary, - context.conduitTheme.buttonPrimary.withValues( - alpha: 0.8, - ), - ], - ), - borderRadius: BorderRadius.circular( - AppBorderRadius.round, - ), - boxShadow: ConduitShadows.glow, - ), - child: Icon( - Platform.isIOS - ? CupertinoIcons.chat_bubble_2 - : Icons.chat, - size: Spacing.xxxl - Spacing.xs, - color: context.conduitTheme.textInverse, - ), - ) - .animate() - .fadeIn( - duration: const Duration(milliseconds: 220), + final greetingDisplay = greetingText ?? ''; + + return MediaQuery.removeViewInsets( + context: context, + removeBottom: true, + child: SizedBox( + width: double.infinity, + height: constraints.maxHeight, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: Spacing.lg), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + SizedBox( + height: greetingHeight, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 260), curve: Curves.easeOutCubic, - ) - .then() - .shimmer(duration: const Duration(milliseconds: 1200)), - - const SizedBox(height: Spacing.xl), - - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - switchInCurve: Curves.easeOutCubic, - switchOutCurve: Curves.easeInCubic, - transitionBuilder: (child, animation) => - FadeTransition(opacity: animation, child: child), - child: Text( - l10n.onboardStartTitle(greetingName), - key: ValueKey(greetingName), - style: theme.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w600, - color: context.conduitTheme.textPrimary, + opacity: _greetingReady ? 1 : 0, + child: Align( + alignment: Alignment.center, + child: Text( + _greetingReady ? greetingDisplay : '', + style: greetingStyle, + textAlign: TextAlign.center, + ), + ), ), - textAlign: TextAlign.center, ), - ), - ], + ], + ), ), ), ); @@ -1127,6 +1128,7 @@ class _ChatPageState extends ConsumerState { } }); } + _lastKeyboardVisible = keyboardVisible; // Auto-select model when in reviewer mode with no selection @@ -1138,19 +1140,14 @@ class _ChatPageState extends ConsumerState { // Focus composer on app startup once if (!_didStartupFocus) { + _didStartupFocus = true; WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) return; - final current = ref.read(inputFocusTriggerProvider); - // Immediate focus bump - ref.read(inputFocusTriggerProvider.notifier).set(current + 1); - // Second bump shortly after to overcome route/IME timing - Future.delayed(const Duration(milliseconds: 120), () { + Future.delayed(const Duration(milliseconds: 200), () { if (!mounted) return; - final cur2 = ref.read(inputFocusTriggerProvider); - ref.read(inputFocusTriggerProvider.notifier).set(cur2 + 1); + final current = ref.read(inputFocusTriggerProvider); + ref.read(inputFocusTriggerProvider.notifier).set(current + 1); }); }); - _didStartupFocus = true; } return ErrorBoundary( diff --git a/lib/features/chat/widgets/modern_chat_input.dart b/lib/features/chat/widgets/modern_chat_input.dart index 2d2ef63..f2b03f1 100644 --- a/lib/features/chat/widgets/modern_chat_input.dart +++ b/lib/features/chat/widgets/modern_chat_input.dart @@ -86,6 +86,7 @@ class _ModernChatInputState extends ConsumerState final TextEditingController _controller = TextEditingController(); final FocusNode _focusNode = FocusNode(); + bool _pendingFocus = false; bool _isRecording = false; // final String _voiceInputText = ''; bool _hasText = false; // track locally without rebuilding on each keystroke @@ -146,6 +147,7 @@ class _ModernChatInputState extends ConsumerState _controller.removeListener(_handleComposerChanged); _controller.dispose(); _focusNode.dispose(); + _pendingFocus = false; _voiceStreamSubscription?.cancel(); _intensitySub?.cancel(); _textSub?.cancel(); @@ -154,11 +156,18 @@ class _ModernChatInputState extends ConsumerState } void _ensureFocusedIfEnabled() { - if (!widget.enabled) return; - if (!_focusNode.hasFocus) { - // Use FocusNode directly to avoid depending on Inherited widgets - _focusNode.requestFocus(); + if (!widget.enabled || _focusNode.hasFocus || _pendingFocus) { + return; } + + _pendingFocus = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _pendingFocus = false; + if (widget.enabled && !_focusNode.hasFocus) { + _focusNode.requestFocus(); + } + }); } @override @@ -1064,11 +1073,9 @@ class _ModernChatInputState extends ConsumerState }, ), }, - child: TweenAnimationBuilder( - tween: Tween(begin: 0.0, end: isActive ? 1.0 : 0.0), - duration: const Duration(milliseconds: 180), - curve: Curves.easeOutCubic, - builder: (context, factor, child) { + child: Builder( + builder: (context) { + final double factor = isActive ? 1.0 : 0.0; final Color animatedPlaceholder = Color.lerp( placeholderBase, placeholderFocused,