Files
meshcore-open/lib/helpers/chat_scroll_controller.dart
T
Dmitry Polshakov 3ea2e4763e fix(chat): fix jump-to-oldest-unread scroll not reaching target message
- Pass initialUnreadCount to chat screens before markRead clears it
- Use two-phase scroll: jumpTo estimated offset to build lazy items,
  then ensureVisible for precise positioning
- Await ensureVisible before clearing scroll guard to prevent
  scrollToBottomIfAtBottom from overriding the animation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 14:07:02 +03:00

88 lines
2.6 KiB
Dart

import 'package:flutter/material.dart';
class ChatScrollController extends ScrollController {
final ValueNotifier<bool> showJumpToBottom = ValueNotifier(false);
VoidCallback? onScrollNearTop;
static const _bottomThreshold = 100.0;
static const _topThreshold = 50.0;
ChatScrollController() {
addListener(_handleScroll);
}
void _handleScroll() {
if (!hasClients) return;
final pos = position;
// With reverse: true, position 0 is bottom, maxScrollExtent is top
// Show jump button when scrolled away from bottom (position > threshold)
final isAtBottom = pos.pixels <= _bottomThreshold;
if (showJumpToBottom.value == isAtBottom) {
showJumpToBottom.value = !isAtBottom;
}
// Pagination trigger when scrolled near top (maxScrollExtent)
if (pos.pixels >= pos.maxScrollExtent - _topThreshold) {
onScrollNearTop?.call();
}
}
void jumpToBottom() {
if (hasClients && position.maxScrollExtent > 0) {
animateTo(
0, // With reverse: true, position 0 is bottom
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
}
void handleKeyboardOpen() {
// Simple: just scroll to bottom when keyboard opens
if (hasClients) {
animateTo(
0, // With reverse: true, position 0 is bottom
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
}
/// Jumps toward an off-screen message so that lazy ListView.builder builds
/// items near it. Only visible + cacheExtent items have real heights, so we
/// use proportion of maxScrollExtent (itself an estimate from built items'
/// avg height). Call [onJumped] on the next frame to ensureVisible/scroll
/// to the exact target.
void jumpToEstimatedOffset({
required int unreadCount,
required int totalMessages,
required VoidCallback onJumped,
}) {
if (!hasClients || totalMessages == 0) return;
final maxExtent = position.maxScrollExtent;
final jumpOffset = maxExtent * (unreadCount / totalMessages);
if (jumpOffset > 100) {
jumpTo(jumpOffset);
}
WidgetsBinding.instance.addPostFrameCallback((_) => onJumped());
}
void scrollToBottomIfAtBottom() {
// Only scroll if jump button is NOT showing (i.e., already at bottom)
if (!showJumpToBottom.value && hasClients && position.maxScrollExtent > 0) {
animateTo(
0, // With reverse: true, position 0 is bottom
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
}
@override
void dispose() {
showJumpToBottom.dispose();
super.dispose();
}
}