refactor: enhance chat message handling and scrolling behavior
- Added `sendMessageWithContainer` function to facilitate message sending with a ProviderContainer. - Updated `_ChatPageState` to improve scroll behavior, ensuring smoother auto-scrolling when near the bottom of the chat. - Refactored scroll logic to simplify conditions for showing and hiding the scroll-to-bottom button. - Adjusted the `OptimizedList` widget to correctly handle item indexing based on the reverse property, enhancing list performance and usability.
This commit is contained in:
@@ -1447,6 +1447,15 @@ Future<void> sendMessageFromService(
|
|||||||
await _sendMessageInternal(ref, message, attachments, toolIds);
|
await _sendMessageInternal(ref, message, attachments, toolIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> sendMessageWithContainer(
|
||||||
|
ProviderContainer container,
|
||||||
|
String message,
|
||||||
|
List<String>? attachments, [
|
||||||
|
List<String>? toolIds,
|
||||||
|
]) async {
|
||||||
|
await _sendMessageInternal(container, message, attachments, toolIds);
|
||||||
|
}
|
||||||
|
|
||||||
// Internal send message implementation
|
// Internal send message implementation
|
||||||
Future<void> _sendMessageInternal(
|
Future<void> _sendMessageInternal(
|
||||||
dynamic ref,
|
dynamic ref,
|
||||||
|
|||||||
@@ -372,10 +372,9 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
// Scroll to bottom after enqueuing (only if user was near bottom)
|
// Scroll to bottom after enqueuing (only if user was near bottom)
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (_scrollController.hasClients) {
|
if (_scrollController.hasClients) {
|
||||||
final maxScroll = _scrollController.position.maxScrollExtent;
|
|
||||||
final currentScroll = _scrollController.position.pixels;
|
final currentScroll = _scrollController.position.pixels;
|
||||||
// Only auto-scroll if user was already near the bottom (within 300px)
|
// Only auto-scroll if user was already near the bottom (within 300px)
|
||||||
if (maxScroll - currentScroll < 300) {
|
if (currentScroll <= 300) {
|
||||||
_scrollToBottom();
|
_scrollToBottom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -543,26 +542,18 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
_scrollDebounceTimer = Timer(const Duration(milliseconds: 80), () {
|
_scrollDebounceTimer = Timer(const Duration(milliseconds: 80), () {
|
||||||
if (!mounted || _isDeactivated || !_scrollController.hasClients) return;
|
if (!mounted || _isDeactivated || !_scrollController.hasClients) return;
|
||||||
|
|
||||||
final maxScroll = _scrollController.position.maxScrollExtent;
|
|
||||||
final currentScroll = _scrollController.position.pixels;
|
final currentScroll = _scrollController.position.pixels;
|
||||||
|
final maxScroll = _scrollController.position.maxScrollExtent;
|
||||||
|
|
||||||
// Hysteresis thresholds to avoid flicker
|
const double showThreshold = 300.0;
|
||||||
const double showThreshold =
|
const double hideThreshold = 150.0;
|
||||||
300.0; // show when farther than this from bottom
|
|
||||||
const double hideThreshold =
|
|
||||||
150.0; // hide when within this distance of bottom
|
|
||||||
|
|
||||||
final bool farFromBottom = currentScroll < (maxScroll - showThreshold);
|
final bool farFromBottom = currentScroll > showThreshold;
|
||||||
final bool nearBottom = currentScroll >= (maxScroll - hideThreshold);
|
final bool nearBottom = currentScroll <= hideThreshold;
|
||||||
|
|
||||||
bool showButton;
|
final bool showButton = _showScrollToBottom
|
||||||
if (_showScrollToBottom) {
|
? !nearBottom && maxScroll > showThreshold
|
||||||
// Currently shown: keep it until we are near the bottom
|
: farFromBottom && maxScroll > showThreshold;
|
||||||
showButton = !nearBottom && maxScroll > showThreshold;
|
|
||||||
} else {
|
|
||||||
// Currently hidden: only show when far from bottom
|
|
||||||
showButton = farFromBottom && maxScroll > showThreshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showButton != _showScrollToBottom && mounted && !_isDeactivated) {
|
if (showButton != _showScrollToBottom && mounted && !_isDeactivated) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -575,17 +566,15 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
void _scrollToBottom({bool smooth = true}) {
|
void _scrollToBottom({bool smooth = true}) {
|
||||||
if (!_scrollController.hasClients) return;
|
if (!_scrollController.hasClients) return;
|
||||||
|
|
||||||
final maxScroll = _scrollController.position.maxScrollExtent;
|
final target = 0.0;
|
||||||
if (maxScroll <= 0) return;
|
|
||||||
|
|
||||||
if (smooth) {
|
if (smooth) {
|
||||||
_scrollController.animateTo(
|
_scrollController.animateTo(
|
||||||
maxScroll,
|
target,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeOutCubic,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
_scrollController.jumpTo(maxScroll);
|
_scrollController.jumpTo(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -742,6 +731,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
scrollController: _scrollController,
|
scrollController: _scrollController,
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
items: messages,
|
items: messages,
|
||||||
|
reverse: true,
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
Spacing.lg,
|
Spacing.lg,
|
||||||
Spacing.md,
|
Spacing.md,
|
||||||
@@ -1029,9 +1019,8 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (_scrollController.hasClients) {
|
if (_scrollController.hasClients) {
|
||||||
final maxScroll = _scrollController.position.maxScrollExtent;
|
|
||||||
final currentScroll = _scrollController.position.pixels;
|
final currentScroll = _scrollController.position.pixels;
|
||||||
if (maxScroll - currentScroll < 300) {
|
if (currentScroll <= 300) {
|
||||||
_scrollToBottom(smooth: true);
|
_scrollToBottom(smooth: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import 'enhanced_attachment.dart';
|
|||||||
import 'package:conduit/shared/widgets/chat_action_button.dart';
|
import 'package:conduit/shared/widgets/chat_action_button.dart';
|
||||||
import '../../../shared/widgets/model_avatar.dart';
|
import '../../../shared/widgets/model_avatar.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import '../providers/chat_providers.dart' show sendMessage;
|
import '../providers/chat_providers.dart' show sendMessageWithContainer;
|
||||||
import '../../../core/utils/debug_logger.dart';
|
import '../../../core/utils/debug_logger.dart';
|
||||||
import 'sources/openwebui_sources.dart';
|
import 'sources/openwebui_sources.dart';
|
||||||
import '../providers/assistant_response_builder_provider.dart';
|
import '../providers/assistant_response_builder_provider.dart';
|
||||||
@@ -70,7 +70,8 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await sendMessage(ref, trimmed, null);
|
final container = ProviderScope.containerOf(context, listen: false);
|
||||||
|
await sendMessageWithContainer(container, trimmed, null);
|
||||||
} catch (err, stack) {
|
} catch (err, stack) {
|
||||||
DebugLogger.log(
|
DebugLogger.log(
|
||||||
'Failed to send follow-up: $err',
|
'Failed to send follow-up: $err',
|
||||||
|
|||||||
@@ -146,6 +146,8 @@ class _OptimizedListState<T> extends ConsumerState<OptimizedList<T>> {
|
|||||||
? const AlwaysScrollableScrollPhysics()
|
? const AlwaysScrollableScrollPhysics()
|
||||||
: const ClampingScrollPhysics());
|
: const ClampingScrollPhysics());
|
||||||
|
|
||||||
|
final reverse = widget.reverse;
|
||||||
|
|
||||||
if (widget.separatorBuilder != null) {
|
if (widget.separatorBuilder != null) {
|
||||||
listWidget = ListView.separated(
|
listWidget = ListView.separated(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
@@ -154,7 +156,7 @@ class _OptimizedListState<T> extends ConsumerState<OptimizedList<T>> {
|
|||||||
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
||||||
shrinkWrap: widget.shrinkWrap,
|
shrinkWrap: widget.shrinkWrap,
|
||||||
scrollDirection: widget.scrollDirection,
|
scrollDirection: widget.scrollDirection,
|
||||||
reverse: widget.reverse,
|
reverse: reverse,
|
||||||
cacheExtent: widget.cacheExtent ?? 250.0,
|
cacheExtent: widget.cacheExtent ?? 250.0,
|
||||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
||||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
addRepaintBoundaries: widget.addRepaintBoundaries,
|
||||||
@@ -165,7 +167,7 @@ class _OptimizedListState<T> extends ConsumerState<OptimizedList<T>> {
|
|||||||
return _buildLoadMoreIndicator();
|
return _buildLoadMoreIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
return _buildOptimizedItem(context, index);
|
return _buildOptimizedItem(context, index, reverse: reverse);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -176,7 +178,7 @@ class _OptimizedListState<T> extends ConsumerState<OptimizedList<T>> {
|
|||||||
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
||||||
shrinkWrap: widget.shrinkWrap,
|
shrinkWrap: widget.shrinkWrap,
|
||||||
scrollDirection: widget.scrollDirection,
|
scrollDirection: widget.scrollDirection,
|
||||||
reverse: widget.reverse,
|
reverse: reverse,
|
||||||
cacheExtent: widget.cacheExtent ?? 250.0,
|
cacheExtent: widget.cacheExtent ?? 250.0,
|
||||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
||||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
addRepaintBoundaries: widget.addRepaintBoundaries,
|
||||||
@@ -187,7 +189,7 @@ class _OptimizedListState<T> extends ConsumerState<OptimizedList<T>> {
|
|||||||
return _buildLoadMoreIndicator();
|
return _buildLoadMoreIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
return _buildOptimizedItem(context, index);
|
return _buildOptimizedItem(context, index, reverse: reverse);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -200,15 +202,21 @@ class _OptimizedListState<T> extends ConsumerState<OptimizedList<T>> {
|
|||||||
return listWidget;
|
return listWidget;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOptimizedItem(BuildContext context, int index) {
|
Widget _buildOptimizedItem(
|
||||||
final item = widget.items[index];
|
BuildContext context,
|
||||||
|
int index, {
|
||||||
|
required bool reverse,
|
||||||
|
}) {
|
||||||
|
final effectiveIndex = reverse ? widget.items.length - index - 1 : index;
|
||||||
|
final item = widget.items[effectiveIndex];
|
||||||
|
|
||||||
// Wrap in repaint boundary for performance
|
|
||||||
if (widget.addRepaintBoundaries) {
|
if (widget.addRepaintBoundaries) {
|
||||||
return RepaintBoundary(child: widget.itemBuilder(context, item, index));
|
return RepaintBoundary(
|
||||||
|
child: widget.itemBuilder(context, item, effectiveIndex),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return widget.itemBuilder(context, item, index);
|
return widget.itemBuilder(context, item, effectiveIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLoadMoreIndicator() {
|
Widget _buildLoadMoreIndicator() {
|
||||||
|
|||||||
Reference in New Issue
Block a user