refactor: enhance chat page greeting display and input focus management

- Introduced caching for greeting names to improve user experience during chat interactions.
- Updated greeting display logic to ensure proper visibility based on user state.
- Added a pending focus mechanism in the chat input to manage focus requests more effectively.
- Refactored layout and animation handling for a smoother greeting presentation.
- Improved overall responsiveness and state management in the chat page.
This commit is contained in:
cogwheel0
2025-10-02 22:38:28 +05:30
parent 5138491cfa
commit ad3834b43e
2 changed files with 83 additions and 79 deletions

View File

@@ -69,6 +69,8 @@ class _ChatPageState extends ConsumerState<ChatPage> {
bool _shouldAutoScrollToBottom = true; bool _shouldAutoScrollToBottom = true;
bool _autoScrollCallbackScheduled = false; bool _autoScrollCallbackScheduled = false;
bool _pendingConversationScrollReset = false; bool _pendingConversationScrollReset = false;
String? _cachedGreetingName;
bool _greetingReady = false;
String _formatModelDisplayName(String name, {required bool omitProvider}) { String _formatModelDisplayName(String name, {required bool omitProvider}) {
var display = name.trim(); var display = name.trim();
@@ -972,74 +974,73 @@ class _ChatPageState extends ConsumerState<ChatPage> {
); );
final authUser = ref.watch(currentUserProvider2); final authUser = ref.watch(currentUserProvider2);
final user = userFromProfile ?? authUser; 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( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return Padding( final greetingDisplay = greetingText ?? '';
padding: const EdgeInsets.all(Spacing.lg),
child: ConstrainedBox( return MediaQuery.removeViewInsets(
constraints: BoxConstraints(minHeight: constraints.maxHeight), context: context,
removeBottom: true,
child: SizedBox(
width: double.infinity,
height: constraints.maxHeight,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: Spacing.lg),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [ children: [
// Minimal, clean empty state SizedBox(
Container( height: greetingHeight,
width: Spacing.xxl + Spacing.xxxl, child: AnimatedOpacity(
height: Spacing.xxl + Spacing.xxxl, duration: const Duration(milliseconds: 260),
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),
curve: Curves.easeOutCubic, curve: Curves.easeOutCubic,
) opacity: _greetingReady ? 1 : 0,
.then() child: Align(
.shimmer(duration: const Duration(milliseconds: 1200)), alignment: Alignment.center,
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( child: Text(
l10n.onboardStartTitle(greetingName), _greetingReady ? greetingDisplay : '',
key: ValueKey<String>(greetingName), style: greetingStyle,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
color: context.conduitTheme.textPrimary,
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
),
),
], ],
), ),
), ),
),
); );
}, },
); );
@@ -1127,6 +1128,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
} }
}); });
} }
_lastKeyboardVisible = keyboardVisible; _lastKeyboardVisible = keyboardVisible;
// Auto-select model when in reviewer mode with no selection // Auto-select model when in reviewer mode with no selection
@@ -1138,19 +1140,14 @@ class _ChatPageState extends ConsumerState<ChatPage> {
// Focus composer on app startup once // Focus composer on app startup once
if (!_didStartupFocus) { if (!_didStartupFocus) {
_didStartupFocus = true;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(const Duration(milliseconds: 200), () {
if (!mounted) return; if (!mounted) return;
final current = ref.read(inputFocusTriggerProvider); final current = ref.read(inputFocusTriggerProvider);
// Immediate focus bump
ref.read(inputFocusTriggerProvider.notifier).set(current + 1); ref.read(inputFocusTriggerProvider.notifier).set(current + 1);
// Second bump shortly after to overcome route/IME timing
Future.delayed(const Duration(milliseconds: 120), () {
if (!mounted) return;
final cur2 = ref.read(inputFocusTriggerProvider);
ref.read(inputFocusTriggerProvider.notifier).set(cur2 + 1);
}); });
}); });
_didStartupFocus = true;
} }
return ErrorBoundary( return ErrorBoundary(

View File

@@ -86,6 +86,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
final TextEditingController _controller = TextEditingController(); final TextEditingController _controller = TextEditingController();
final FocusNode _focusNode = FocusNode(); final FocusNode _focusNode = FocusNode();
bool _pendingFocus = false;
bool _isRecording = false; bool _isRecording = false;
// final String _voiceInputText = ''; // final String _voiceInputText = '';
bool _hasText = false; // track locally without rebuilding on each keystroke bool _hasText = false; // track locally without rebuilding on each keystroke
@@ -146,6 +147,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
_controller.removeListener(_handleComposerChanged); _controller.removeListener(_handleComposerChanged);
_controller.dispose(); _controller.dispose();
_focusNode.dispose(); _focusNode.dispose();
_pendingFocus = false;
_voiceStreamSubscription?.cancel(); _voiceStreamSubscription?.cancel();
_intensitySub?.cancel(); _intensitySub?.cancel();
_textSub?.cancel(); _textSub?.cancel();
@@ -154,11 +156,18 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
} }
void _ensureFocusedIfEnabled() { void _ensureFocusedIfEnabled() {
if (!widget.enabled) return; if (!widget.enabled || _focusNode.hasFocus || _pendingFocus) {
if (!_focusNode.hasFocus) { return;
// Use FocusNode directly to avoid depending on Inherited widgets }
_pendingFocus = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_pendingFocus = false;
if (widget.enabled && !_focusNode.hasFocus) {
_focusNode.requestFocus(); _focusNode.requestFocus();
} }
});
} }
@override @override
@@ -1064,11 +1073,9 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
}, },
), ),
}, },
child: TweenAnimationBuilder<double>( child: Builder(
tween: Tween<double>(begin: 0.0, end: isActive ? 1.0 : 0.0), builder: (context) {
duration: const Duration(milliseconds: 180), final double factor = isActive ? 1.0 : 0.0;
curve: Curves.easeOutCubic,
builder: (context, factor, child) {
final Color animatedPlaceholder = Color.lerp( final Color animatedPlaceholder = Color.lerp(
placeholderBase, placeholderBase,
placeholderFocused, placeholderFocused,