loadConversationMessages method

Future<void> loadConversationMessages({
  1. 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);
    });
  }
}