loadConversationMessages method
Future<void>
loadConversationMessages(
{ - bool reset = false,
})
Implementation
Future<void> loadConversationMessages({bool reset = false}) async {
if (isLoadingConversationPage || isConversationLastPage) {
return;
}
double? previousMaxExtent;
double? previousPixels;
if (!reset && chatScrollController.hasClients) {
previousMaxExtent =
chatScrollController.positions.lastOrNull?.maxScrollExtent;
previousPixels = chatScrollController.positions.lastOrNull?.pixels;
}
isLoadingConversationPage = true;
String url = ApiUrls.queriesUrl(
assistantId,
conversation.value?.id ?? "",
page: conversationPage,
isMarketplace: isMarketplace,
);
try {
await ApiService.call(
url,
RequestType.get,
onSuccess: (response) {
final int currentPage = conversationPage;
List<dynamic> items = response.data['items'] ?? [];
if (items.isNotEmpty) {
List<PupauMessage> queryList = messagesFromLoadedChat(
jsonEncode(items),
);
// When resetting (initial load), we build the list "from scratch"
// by inserting at the front so that, after the UI reverses the
// list, the most recent messages appear at the bottom.
//
// When loading older pages (scrolling up), we append at the end so
// that, after reversal, they appear at the top of the viewport.
final bool isInitialLoad = reset;
for (PupauMessage message in queryList) {
PupauMessage userMessage = MessageService.getUserLoadedMessage(
message,
);
PupauMessage assistantMessage =
MessageService.getAssistantLoadedMessage(message);
if (isInitialLoad) {
if (isFirstMessageInGroup(message.groupId)) {
messages.insert(0, userMessage);
}
messages.insert(0, assistantMessage);
} else {
if (isFirstMessageInGroup(message.groupId)) {
messages.add(userMessage);
}
messages.add(assistantMessage);
}
}
}
// Track how many queries have been loaded so far (informational only).
conversationItemsLoaded += items.length;
// We consider pagination finished when:
// - the API returns no items, or
// - we've just loaded page 0 (the oldest page).
if (items.isEmpty || currentPage == 0) {
isConversationLastPage = true;
} else {
isConversationLastPage = false;
}
// Move to the previous page index (older messages) for the next load,
// unless we've already reached the oldest page.
if (!isConversationLastPage && conversationPage > 0) {
conversationPage--;
}
// If the last page (initial load) has very few messages, eagerly
// load one more older page so the chat isn't almost empty.
// We only do this on the first (reset) load to avoid chaining
// multiple extra page requests.
if (reset && items.length <= 5 && currentPage > 0) {
// Fire and forget; this call will run with reset = false and
// append older messages at the top (tail) of the history.
loadConversationMessages();
}
messages.refresh();
update();
},
onError: (error) {
update();
},
);
} finally {
isLoadingConversationPage = false;
update();
}
if (!reset &&
previousPixels != null &&
previousMaxExtent != null &&
chatScrollController.hasClients) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!chatScrollController.hasClients) return;
final position = chatScrollController.positions.lastOrNull;
if (position == null) return;
double newMax = position.maxScrollExtent;
double minExtent = position.minScrollExtent;
double offsetDiff = newMax - previousMaxExtent!;
double targetOffset = previousPixels! + offsetDiff;
// Clamp targetOffset to valid bounds first, then apply safety margin to prevent bounce
// Use a small safety margin (0.5px) to prevent floating point precision issues
final safetyMargin = 0.5;
targetOffset = targetOffset.clamp(minExtent, newMax);
// Apply safety margin to prevent overshoot that causes bounce
if (targetOffset >= newMax - safetyMargin) {
targetOffset = newMax - safetyMargin;
}
// Use position.jumpTo() which respects physics constraints better
position.jumpTo(targetOffset);
});
}
}