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:
@@ -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,72 +974,71 @@ 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,
|
||||||
child: Column(
|
removeBottom: true,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: SizedBox(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
width: double.infinity,
|
||||||
children: [
|
height: constraints.maxHeight,
|
||||||
// Minimal, clean empty state
|
child: Padding(
|
||||||
Container(
|
padding: const EdgeInsets.symmetric(horizontal: Spacing.lg),
|
||||||
width: Spacing.xxl + Spacing.xxxl,
|
child: Column(
|
||||||
height: Spacing.xxl + Spacing.xxxl,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
decoration: BoxDecoration(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
gradient: LinearGradient(
|
mainAxisSize: MainAxisSize.max,
|
||||||
begin: Alignment.topLeft,
|
children: [
|
||||||
end: Alignment.bottomRight,
|
SizedBox(
|
||||||
colors: [
|
height: greetingHeight,
|
||||||
context.conduitTheme.buttonPrimary,
|
child: AnimatedOpacity(
|
||||||
context.conduitTheme.buttonPrimary.withValues(
|
duration: const Duration(milliseconds: 260),
|
||||||
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,
|
||||||
|
child: Text(
|
||||||
const SizedBox(height: Spacing.xl),
|
_greetingReady ? greetingDisplay : '',
|
||||||
|
style: greetingStyle,
|
||||||
AnimatedSwitcher(
|
textAlign: TextAlign.center,
|
||||||
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<String>(greetingName),
|
|
||||||
style: theme.textTheme.headlineSmall?.copyWith(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: context.conduitTheme.textPrimary,
|
|
||||||
),
|
),
|
||||||
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((_) {
|
||||||
if (!mounted) return;
|
Future.delayed(const Duration(milliseconds: 200), () {
|
||||||
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), () {
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final cur2 = ref.read(inputFocusTriggerProvider);
|
final current = ref.read(inputFocusTriggerProvider);
|
||||||
ref.read(inputFocusTriggerProvider.notifier).set(cur2 + 1);
|
ref.read(inputFocusTriggerProvider.notifier).set(current + 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
_didStartupFocus = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ErrorBoundary(
|
return ErrorBoundary(
|
||||||
|
|||||||
@@ -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
|
|
||||||
_focusNode.requestFocus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_pendingFocus = true;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
_pendingFocus = false;
|
||||||
|
if (widget.enabled && !_focusNode.hasFocus) {
|
||||||
|
_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,
|
||||||
|
|||||||
Reference in New Issue
Block a user