implemented backward pagination for chat history with on-scroll prefetching

This commit is contained in:
2026-07-17 20:14:47 +02:00
parent 101e7c909c
commit f3cd7896d9
7 changed files with 270 additions and 40 deletions
+59 -16
View File
@@ -42,9 +42,15 @@ class ChatView extends StatefulWidget {
class _ChatViewState extends State<ChatView> with RouteAware {
final ItemScrollController _itemScrollController = ItemScrollController();
final ItemPositionsListener _positionsListener =
ItemPositionsListener.create();
final TextEditingController _searchTextController = TextEditingController();
final Map<int, int> _matchIndices = {};
// Number of rows currently rendered; kept in sync in build so the scroll
// listener can tell when the oldest row (highest index, reverse list) nears.
int _itemCount = 0;
bool _searchActive = false;
String _searchQuery = '';
List<ChatSearchMatch> _matches = const [];
@@ -63,9 +69,24 @@ class _ChatViewState extends State<ChatView> with RouteAware {
super.initState();
_chatBlocRef = context.read<ChatBloc>();
_chatListBlocRef = context.read<ChatListBloc>();
_positionsListener.itemPositions.addListener(_onScrollPositions);
NotificationTasks.clearNotificationsForChat(widget.room.token);
}
/// Loads the next older block once the top of the list comes into view.
void _onScrollPositions() {
final positions = _positionsListener.itemPositions.value;
if (positions.isEmpty) return;
// reverse:true → the highest index is the oldest message (top of screen).
// Prefetch ~a screenful ahead so the next block is usually already merged
// before the user reaches the top — the load stays invisible.
final maxIndex = positions.map((p) => p.index).reduce(math.max);
if (maxIndex < _itemCount - _kLoadOlderPrefetchRows) return;
final data = _chatBlocRef?.state.data;
if (data == null || !data.hasMoreOld || data.isLoadingOlder) return;
_chatBlocRef?.loadOlder();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
@@ -93,6 +114,7 @@ class _ChatViewState extends State<ChatView> with RouteAware {
if (_subscribedRoute != null) {
AppRoutes.chatRouteObserver.unsubscribe(this);
}
_positionsListener.itemPositions.removeListener(_onScrollPositions);
_markAsReadFinal();
_chatBlocRef?.leaveChat(widget.room.token);
_searchTextController.dispose();
@@ -281,22 +303,6 @@ class _ChatViewState extends State<ChatView> with RouteAware {
);
}
if (response.data.length >= 200) {
messages.insert(
0,
ChatBubble(
isSender: false,
bubbleData: GetChatResponseObject.getTextDummy(
'Zurzeit können in dieser App nur die letzten 200 vergangenen Nachrichten angezeigt werden. '
'Um ältere Nachrichten abzurufen verwende die Webversion unter https://cloud.marianum-fulda.de',
),
chatData: widget.room,
refetch: ({bool renew = false}) => _refresh(),
),
);
chronologicalMatchIndex.updateAll((_, v) => v + 1);
}
final total = messages.length;
_matchIndices
..clear()
@@ -373,9 +379,27 @@ class _ChatViewState extends State<ChatView> with RouteAware {
final items = _buildMessages(
state.chatResponse!,
).reversed.toList();
// reverse:true renders index 0 at the bottom, so the top
// marker (spinner / start-of-chat) goes at the end.
if (state.isLoadingOlder) {
items.add(const _LoadingOlderIndicator());
} else if (!state.hasMoreOld) {
items.add(
ChatBubble(
isSender: false,
bubbleData: GetChatResponseObject.getTextDummy(
'Anfang des Chats',
),
chatData: widget.room,
refetch: ({bool renew = false}) => _refresh(),
),
);
}
_itemCount = items.length;
return ScrollablePositionedList.builder(
reverse: true,
itemScrollController: _itemScrollController,
itemPositionsListener: _positionsListener,
itemCount: items.length,
itemBuilder: (ctx, idx) => items[idx],
);
@@ -405,3 +429,22 @@ class _ChatViewState extends State<ChatView> with RouteAware {
);
}
}
/// How many rows before the oldest one the older-history prefetch kicks in.
const _kLoadOlderPrefetchRows = 15;
class _LoadingOlderIndicator extends StatelessWidget {
const _LoadingOlderIndicator();
@override
Widget build(BuildContext context) => const Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: Center(
child: SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
);
}