diff --git a/TODO.md b/TODO.md index f346dff..3ec74c1 100644 --- a/TODO.md +++ b/TODO.md @@ -25,7 +25,7 @@ ## Frontend - [x] Mobile - Implement pull to refresh on the notifications and devices list -- [ ] Mobile - reduce lists length (# of items) so they fit on a phone in one screen (use iPhone latest gen and Google phone latest gen) +- [x] Mobile - reduce lists length (# of items) so they fit on a phone in one screen (use iPhone latest gen and Google phone latest gen) - [ ] Mobile - In the devices list the filters and sort buttons overlap (dont fit in the screen) - [ ] When changing pages (either list) the items should change to placeholders while loading - [ ] The notifications list should not refresh coldly every time. It should add/remove notifications with an animation as if a stack diff --git a/frontend/lib/devices/device_list.dart b/frontend/lib/devices/device_list.dart index edd5413..7badb59 100644 --- a/frontend/lib/devices/device_list.dart +++ b/frontend/lib/devices/device_list.dart @@ -17,7 +17,11 @@ import 'device_list_filter.dart'; import 'device_list_rows.dart'; import 'device_list_sort.dart'; -const _pageSize = 10; +// Phones show fewer devices so the list and its pagination controls fit on +// screen at once on the common current phones (e.g. iPhone 15, Pixel 8); the +// wider table layout has the vertical room for a full page. +const _phonePageSize = 6; +const _widePageSize = 10; class DeviceList extends StatefulWidget { const DeviceList({super.key}); @@ -40,14 +44,18 @@ class _DeviceListState extends State with RouteAware { int _currentPage = 0; bool _hasNextPage = false; + bool _didInitialFetch = false; CancelToken? _fetchToken; final ScrollController _scrollController = ScrollController(); + int get _pageSize => MediaQuery.sizeOf(context).width < Breakpoints.medium + ? _phonePageSize + : _widePageSize; + @override void initState() { super.initState(); _ownerController.addListener(_onOwnerChanged); - _fetchPage(0); } @override @@ -57,6 +65,12 @@ class _DeviceListState extends State with RouteAware { if (route is ModalRoute) { routeObserver.subscribe(this, route); } + // Deferred from initState so the page size can read the screen width from + // MediaQuery, which is only available once dependencies are in place. + if (!_didInitialFetch) { + _didInitialFetch = true; + _fetchPage(0); + } } @override diff --git a/frontend/lib/home/notifications_list.dart b/frontend/lib/home/notifications_list.dart index 139ae8c..a4f03e9 100644 --- a/frontend/lib/home/notifications_list.dart +++ b/frontend/lib/home/notifications_list.dart @@ -14,7 +14,11 @@ import '../widgets/pagination_bar.dart'; import '../widgets/skeleton.dart'; import 'notification_card.dart'; -const _pageSize = 5; +// Phones show fewer notifications so the list and its pagination controls fit +// on screen at once on the common current phones (e.g. iPhone 15, Pixel 8); +// wider layouts have the vertical room for a couple more. +const _phonePageSize = 4; +const _widePageSize = 5; enum _NotificationFilter { newOnly('New'), @@ -47,8 +51,13 @@ class _NotificationsListState extends State Timer? _notificationTimer; int _currentPage = 0; + bool _didInitialFetch = false; List _items = []; bool _isLoading = false; + + int get _pageSize => MediaQuery.sizeOf(context).width < Breakpoints.medium + ? _phonePageSize + : _widePageSize; bool _hasNextPage = false; String? _error; CancelToken? _fetchToken; @@ -58,7 +67,6 @@ class _NotificationsListState extends State void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); - _fetchPage(0); _startTimer(); } @@ -69,6 +77,12 @@ class _NotificationsListState extends State if (route is ModalRoute) { routeObserver.subscribe(this, route); } + // Deferred from initState so the page size can read the screen width from + // MediaQuery, which is only available once dependencies are in place. + if (!_didInitialFetch) { + _didInitialFetch = true; + _fetchPage(0); + } } @override diff --git a/frontend/test/widget/notifications_list_test.dart b/frontend/test/widget/notifications_list_test.dart index d259953..28cd3f7 100644 --- a/frontend/test/widget/notifications_list_test.dart +++ b/frontend/test/widget/notifications_list_test.dart @@ -62,19 +62,21 @@ void main() { tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); + // The 400px-wide viewport above is a phone, so the list uses the smaller + // phone page size of 4 (requesting page_limit 5 to detect a next page). List> page(int firstId) => List.generate( - 6, + 5, (i) => notificationJson(id: firstId + i, title: 'Item ${firstId + i}'), ); adapter.onGet( '/notifications', (server) => server.reply(200, page(1)), - queryParameters: {'is_new': true, 'page_offset': 0, 'page_limit': 6}, + queryParameters: {'is_new': true, 'page_offset': 0, 'page_limit': 5}, ); adapter.onGet( '/notifications', - (server) => server.reply(200, page(7)), - queryParameters: {'is_new': true, 'page_offset': 5, 'page_limit': 6}, + (server) => server.reply(200, page(6)), + queryParameters: {'is_new': true, 'page_offset': 4, 'page_limit': 5}, ); await tester.pumpWidget( @@ -119,7 +121,7 @@ void main() { adapter.onGet( '/notifications', (server) => server.reply(200, items), - queryParameters: {'is_new': true, 'page_offset': 0, 'page_limit': 6}, + queryParameters: {'is_new': true, 'page_offset': 0, 'page_limit': 5}, ); await tester.pumpWidget(