high priority medium complexity frontend pending frontend specialist Tier 4

Acceptance Criteria

Dragging the notification list downward triggers a RefreshIndicator spinner and dispatches RefreshNotifications event to the BLoC, resetting the page cursor to the first page
After a successful refresh, the list is fully replaced with the latest notifications and the scroll position returns to the top
A ScrollController listener detects when scroll offset is within 200px of maxScrollExtent and dispatches LoadNextPage if no page load is already in progress
The BLoC correctly appends newly fetched notifications to the existing list without duplicating already-displayed items
A CircularProgressIndicator widget is rendered at the bottom of the ListView while the next page is being fetched, and is removed once the response resolves
When the user reaches the last page (no more cursor returned), no further LoadNextPage events are dispatched and the bottom indicator is not shown
Changing the active filter chip resets the page cursor to null, clears the existing list, and triggers a fresh first-page load
If a pagination request fails, an error snackbar is shown, the bottom indicator is removed, and the existing list remains intact so the user can retry
Pull-to-refresh works correctly when a filter is active, fetching the first page for that filter
ScrollController is disposed in the widget's dispose() method to prevent memory leaks

Technical Requirements

frameworks
Flutter
BLoC
apis
Supabase REST API — paginated notifications endpoint with cursor/offset parameters
data models
Notification
NotificationPage (list + nextCursor)
DateRangeFilter
NotificationFilterType
performance requirements
Page size should be 20 items to balance load time and scroll smoothness
ScrollController listener must debounce or guard with an isLoadingNextPage flag to prevent duplicate requests
List rendering must use ListView.builder (lazy) — never ListView with children list
security requirements
Paginated queries must include the authenticated user's scope to prevent cross-user data leakage
Cursor values must be treated as opaque server tokens — never manipulated client-side
ui components
RefreshIndicator wrapping the scrollable list
CircularProgressIndicator in a centered Padding widget at the list footer
SliverList or ListView.builder for lazy rendering
ScrollController attached to the list

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Attach the ScrollController in initState and add a listener: `if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200 && !state.isLoadingNextPage && state.hasMore) { context.read().add(LoadNextPage()); }`. Store isLoadingNextPage and hasMore flags in the BLoC state to act as a guard. The BLoC should track `String? nextCursor` in its state; pass it to the service on each LoadNextPage.

On filter change, emit a state with cursor = null, items = [], then immediately fetch page 1. Avoid calling `setState` in the scroll listener — let BLoC state drive rebuilds via BlocBuilder. Use a `GlobalKey` only if programmatic refresh is needed (e.g., on first load error). Dispose the ScrollController in the widget's dispose() to prevent leaked listeners.

Testing Requirements

Write flutter_test widget tests covering: (1) RefreshIndicator callback dispatches RefreshNotifications and resets cursor, (2) ScrollController triggers LoadNextPage when scrolled to within 200px of the bottom, (3) BLoC state transitions — loading → appended success → no more pages, (4) filter change clears list and resets cursor before fetching, (5) duplicate LoadNextPage guard — rapid scroll does not fire multiple events, (6) error state during pagination shows snackbar without clearing existing list. Use bloc_test package to test BLoC event/state sequences in isolation. Target 80%+ branch coverage for the BLoC and the screen widget.

Component
Notification Centre Screen
ui medium
Epic Risks (3)
medium impact medium prob integration

If a referenced entity (contact, certification, activity) has been deleted or its RLS policy now excludes the current user, the deep link handler may navigate to a screen that renders in an error state or throws an unhandled exception.

Mitigation & Contingency

Mitigation: The deep link handler must perform a lightweight existence check (HEAD request or minimal SELECT) before pushing the route. Define a contract with each destination screen for how to handle a not-found entity ID passed as a route parameter.

Contingency: If the existence check itself fails (network error), navigate to the destination screen anyway and let it handle the error gracefully with its own error state; do not block navigation for network timeouts.

medium impact low prob technical

If the tab badge widget triggers a full rebuild of the bottom navigation bar on every unread count change, it will cause visible jank on devices with many active Realtime events (e.g., org admins receiving org-wide alerts).

Mitigation & Contingency

Mitigation: Scope the badge widget to a dedicated BlocSelector that rebuilds only when the unread count value changes, not on any BLoC state emission. Use RepaintBoundary to isolate the badge from the rest of the nav bar.

Contingency: If performance issues persist, debounce badge updates to a maximum of one rebuild per 500ms and display the last known count during the debounce window.

high impact medium prob scope

Complex swipe-to-mark-read gestures and dynamic list updates may conflict with VoiceOver/TalkBack navigation patterns, particularly for Blindeforbundet users who rely exclusively on screen readers.

Mitigation & Contingency

Mitigation: Provide a dedicated accessibility action (Semantics.onTap / CustomSemanticsAction) for mark-as-read on each list item so screen reader users do not need the swipe gesture. Test with VoiceOver on iOS and TalkBack on Android before each release.

Contingency: If the swipe gesture proves incompatible with assistive technologies, disable it when a screen reader is detected (via ScreenReaderDetectionService) and rely solely on the tap-to-read and accessible action pathways.