Add infinite scroll and pull to refresh#499
Conversation
- refactor DRY widget - delete redundant provider - fix bad number formatting locale
…into beast/language-support
- use intl from sdk - import legacy provider
Setting already cleared in substarte service logout
- implement infinite scroll - implement pull to refresh
…into beast/language-support
…etwork/quantus-apps into beast/integrate-infinite-scroll-and-refresh-to-activity-screen
|
I've done a thorough review of PR #499. Here's my analysis. PR #499 Review — Add infinite scroll and pull to refreshOverall this is a clean, well-scoped UX improvement. The core mechanics (shared Behavior issues1. Pull-to-refresh wipes the list to skeletons (UX regression). final targetAccountIds = _getAccountIds();
if (targetAccountIds.isEmpty) {
state = PaginationState.initial().copyWith(hasMore: false);
return;
}
// Reset to initial state to show loading
state = PaginationState.initial();
await _fetchPage(targetAccountIds);After that, the combined provider returns Pick one of:
2. void _onScroll() {
if (!_scrollController.hasClients) return;
final pos = _scrollController.position;
if (pos.pixels < pos.maxScrollExtent - _loadMoreThreshold) return;
final pagination = ref.read(activeAccountPaginationProvider(_filterOption));
if (pagination == null || pagination.isFetching || !pagination.hasMore) return;
activeAccountPaginationNotifier(ref, _filterOption)?.fetchMore();
}When the list is shorter than the viewport, if (pos.maxScrollExtent <= 0) return;
if (pos.pixels < pos.maxScrollExtent - _loadMoreThreshold) return;If you actually want auto-fill-to-viewport behavior (fetch more until the list fills the screen), do it explicitly in the controller after a fetch completes, not as a side-effect of touching the screen. 3. Bugbot's "filter change keeps scroll offset" — not actually a bug, but worth confirming. You added API / semantics4. hasMore: hasMore ?? this.hasMore,
isFetching: isFetching ?? this.isFetching,
error: error,
stackTrace: stackTrace,This is the right fix for "can't clear the error", but it changes semantics for every call site: any 5. if (pagination.isFetching && pagination.otherTransfers.isEmpty) {
return const AsyncValue.loading();
}You added Minor / style6. Logging inconsistency. Providers now use 7. Naming: 8. Duplicated 9. 10. Things I think are good
RecommendationApprove with the following changes requested:
The rest (naming, DRY of Want me to apply any of these as edits, or open follow-up comments on the PR? Per your rules, I won't post to GitHub or push without your go-ahead. |
|
Re-reviewed against the two new commits ( Re-reviewAddressed nicely
Still outstanding from the previous review#1 Pull-to-refresh still flashes skeletons. final targetAccountIds = _getAccountIds();
if (targetAccountIds.isEmpty) {
state = PaginationState.initial().copyWith(hasMore: false);
return;
}
// Reset to initial state to show loading
state = PaginationState.initial();
await _fetchPage(targetAccountIds);So after the user pulls, #2 void _onScroll() {
if (!_scrollController.hasClients) return;
final pos = _scrollController.position;
if (pos.pixels < pos.maxScrollExtent - _loadMoreThreshold) return;When the loaded page doesn't fill the viewport ( if (pos.maxScrollExtent <= 0) return;#5 Loading gate inconsistent with error gate. Both transactions providers still gate loading on if (pagination.isFetching && pagination.otherTransfers.isEmpty) {
return const AsyncValue.loading();
}The error gate uses New issue introduced by
|
|
|
||
| Future<void> fetchMore() async { | ||
| print('UnifiedPaginationController: Fetch more'); | ||
| quantusDebugPrint('UnifiedPaginationController: Fetch more'); |
There was a problem hiding this comment.
Overlapping fetchMore requests
Medium Severity
fetchMore only sets isFetching inside _fetchPage, so two calls can pass the guard at once and issue the same paged request. Offsets can advance twice while the UI dedupes by id, which can skip or reorder history pages.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit f283500. Configure here.
| clearError: true, | ||
| ); | ||
| } catch (e, st) { | ||
| print('Silent refresh failed: $e, $st'); |
There was a problem hiding this comment.
Silent refresh hides PTR errors
Medium Severity
When pull-to-refresh uses silentRefresh, failures in _silentFetchFirstPage are only logged; state keeps prior data and no error, so RefreshIndicator still succeeds and the user is not informed the refresh failed.
Reviewed by Cursor Bugbot for commit f283500. Configure here.
| if (!_scrollController.hasClients) return; | ||
|
|
||
| final pos = _scrollController.position; | ||
| if (pos.maxScrollExtent <= 0 || pos.pixels < pos.maxScrollExtent - _loadMoreThreshold) return; |
There was a problem hiding this comment.
Short list blocks load more
Medium Severity
The _onScroll method's check for maxScrollExtent <= 0 prevents fetchMore from being called. This means if the initial list of transactions fits within the viewport, subsequent pages of activity will not load, even when more data is available.
Reviewed by Cursor Bugbot for commit f283500. Configure here.
| }; | ||
| Future<void> _refresh() async { | ||
| final pagination = ref.read(activeAccountPaginationProvider(_filterOption)); | ||
| if (pagination == null || pagination.isFetching) return; |
There was a problem hiding this comment.
Pull refresh skips while fetching
Medium Severity
_refresh returns immediately when pagination.isFetching is true, so pull-to-refresh completes without awaiting a refresh while load-more is in progress, even though the user expects new data.
Reviewed by Cursor Bugbot for commit f283500. Configure here.
| final pagination = ref.read(activeAccountPaginationProvider(_filterOption)); | ||
| if (pagination == null || pagination.isFetching) return; | ||
|
|
||
| await readActiveAccountPaginationNotifier(ref, _filterOption)?.silentRefresh(); |
There was a problem hiding this comment.
Activity PTR bypasses offline guard
Medium Severity
Activity pull-to-refresh calls silentRefresh, which does not check isOnlineProvider, unlike loadingRefresh used on Home and manual global refresh. Offline pulls still hit the network and can finish with unchanged data while the indicator succeeds.
Reviewed by Cursor Bugbot for commit f283500. Configure here.
| TransactionFilter.send => l10n.activityFilterSend, | ||
| TransactionFilter.receive => l10n.activityFilterReceive, | ||
| }; | ||
| } |
There was a problem hiding this comment.
Unused filter label extension
Low Severity
The TransactionFilterLabel extension and its filterLabel method were introduced but became dead code, as the Activity filter chips that would have used them were removed.
Reviewed by Cursor Bugbot for commit f283500. Configure here.
Re-review (head
|
|
some minor cleanups see above then GTG |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 7 total unresolved issues (including 6 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a84cf86. Configure here.



Summary
Continuation of this PR #493 , closing that one because of broken git history.
Note
Medium Risk
Changes how transaction history loading and errors are surfaced (initial vs load-more), which affects core wallet activity UX; logic is localized but easy to regress on edge cases (empty history, offline refresh).
Overview
Adds pull-to-refresh and infinite scroll on the v2 Activity screen by wiring a
ScrollControllertofetchMore()andsilentRefresh()via newactiveAccountPaginationProvider/readActiveAccountPaginationNotifierhelpers on the active-account filtered pagination controller.Pagination behavior is refined:
PaginationStategainsisLoadingandhasLoadedChainData, pluscopyWith(clearError:)so errors can be cleared explicitly. Transaction list providers only emitAsyncValue.errorwhen the initial chain load fails; load-more failures are logged but existing rows stay visible. Initial loading is keyed offisLoadinginstead ofisFetching && empty.UnifiedPaginationControllerupdates loading flags through init/fetch/refresh paths and wrapssilentRefreshinisFetchingso concurrent fetches are guarded. Activity UI drops the send/receive/all filter chips (still fixed toTransactionFilter.all) and shows a bottom loader while loading more pages; empty state remains pull-to-refreshable.Also adds unit tests for
PaginationState.copyWitherror semantics, aTransactionFilterl10n extension,quantusDebugPrintusage, and excludesbuild/**from analyzer.Reviewed by Cursor Bugbot for commit f46603d. Bugbot is set up for automated code reviews on this repo. Configure here.