diff --git a/backend/src/db/notifications.rs b/backend/src/db/notifications.rs index c985560..aaa0624 100644 --- a/backend/src/db/notifications.rs +++ b/backend/src/db/notifications.rs @@ -6,6 +6,7 @@ use rusqlite::{params, params_from_iter}; use crate::model::notifications::Notification; pub fn list( + is_new: Option, page_offset: Option, page_limit: Option, ) -> Result, DbError> { @@ -13,12 +14,23 @@ pub fn list( let conn = db::get_db_connection(); let mut sql_statement = - "SELECT id, created_on, notification_type, title, body FROM notifications WHERE is_new=1" + "SELECT id, created_on, notification_type, title, body, is_new FROM notifications WHERE 1=1" .to_string(); + let mut params: Vec = Vec::new(); + + // Filters + if is_new.is_some() { + debug!("Adding filter is_new={}", is_new.unwrap()); + sql_statement.push_str(" AND is_new=?"); + + params.push(is_new.unwrap().into()); + } + + // List order sql_statement.push_str(" ORDER BY created_on DESC"); - let mut params: Vec = Vec::new(); + // Paging if page_offset.is_some() && page_limit.is_some() { debug!( "Adding paging to list with offset={} and limit={}", @@ -41,7 +53,7 @@ pub fn list( notification_type: row.get(2)?, title: row.get(3)?, body: row.get(4)?, - is_new: true, + is_new: row.get(5)?, }) })? .collect::>()?; @@ -322,7 +334,7 @@ mod tests { #[tokio::test] async fn test_list() { tests_common::setup().await; - let notifications = list(None, None).unwrap(); + let notifications = list(Some(true), None, None).unwrap(); // There should be at least 3 unread notifications assert!( @@ -408,12 +420,31 @@ mod tests { ); } + #[tokio::test] + async fn test_list_filters() { + tests_common::setup().await; + + // List only new notifications + let notifications = list(Some(true), Some(0), Some(5)).unwrap(); + assert!( + !notifications.iter().any(|item| !item.is_new), + "All notifications should be new" + ); + + // List only old notifications + let notifications = list(Some(false), Some(0), Some(5)).unwrap(); + assert!( + !notifications.iter().any(|item| item.is_new), + "All notifications should be old" + ); + } + #[tokio::test] async fn test_list_pagination() { tests_common::setup().await; // List first page with 2 elements - let first_page = list(Some(0), Some(2)).unwrap(); + let first_page = list(Some(true), Some(0), Some(2)).unwrap(); assert_eq!( first_page.iter().len(), @@ -422,18 +453,10 @@ mod tests { ); // List second page with 2 elements, should only be 1 - let second_page = list(Some(2), Some(2)).unwrap(); + let second_page = list(Some(true), Some(2), Some(2)).unwrap(); assert!( second_page.iter().len() >= 1, "Second page should have at least 1 notification" ); - - // None of the first page elements should be present - for item in first_page.iter() { - assert!( - !second_page.iter().any(|second_item| item == second_item), - "No first page element should be in the second page" - ); - } } } diff --git a/backend/src/web_server/notifications.rs b/backend/src/web_server/notifications.rs index 7c19480..95d4713 100644 --- a/backend/src/web_server/notifications.rs +++ b/backend/src/web_server/notifications.rs @@ -34,10 +34,11 @@ pub async fn read_without_flagging(Path(id): Path) -> Result>, ) -> Result>, StatusCode> { + let is_new: Option = utils::parse_parameter_bool(¶ms, "is_new"); let page_offset: Option = utils::parse_parameter_int(¶ms, "page_offset"); let page_limit: Option = utils::parse_parameter_int(¶ms, "page_limit"); - match db::notifications::list(page_offset, page_limit) { + match db::notifications::list(is_new, page_offset, page_limit) { Ok(value) => Ok(Json(value)), Err(err) => { error!("Error listing notifications: {}", err); diff --git a/frontend/lib/model/notification_type.dart b/frontend/lib/model/notification_type.dart index 7521cf3..da7d640 100644 --- a/frontend/lib/model/notification_type.dart +++ b/frontend/lib/model/notification_type.dart @@ -9,13 +9,13 @@ enum NotificationType { IconData get icon { switch (this) { case newDeviceFound: - return Icons.devices; + return Icons.radar; case deviceOnlineAfterTime: return Icons.timer; case deviceChanged: return Icons.change_circle; case other: - return Icons.priority_high; + return Icons.notification_important; } } diff --git a/frontend/lib/notifications/notification_list.dart b/frontend/lib/notifications/notification_list.dart index f2fd091..935408c 100644 --- a/frontend/lib/notifications/notification_list.dart +++ b/frontend/lib/notifications/notification_list.dart @@ -13,65 +13,128 @@ class NotificationList extends StatefulWidget { } class _NotificationListState extends State { - final _pagingController = PagingController( + int _filterChoice = 1; // 1 => Only new, 2=> Only old, 3=> All + + late final _pagingController = PagingController( getNextPageKey: (state) => state.lastPageIsEmpty ? null : (state.items == null ? 0 : state.items?.length), - fetchPage: (pageKey) => BackendAPI.instance.listNotifications(pageKey), + fetchPage: (pageKey) { + bool? isNew; + + if (_filterChoice == 1) { + isNew = true; + } else if (_filterChoice == 2) { + isNew = false; + } + + return BackendAPI.instance.listNotifications(isNew, pageKey); + }, ); + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Notifications')), + body: + // Paginated list start + PagingListener( + controller: _pagingController, + builder: (context, state, fetchNextPage) => CustomScrollView( + slivers: [ + // Filters go here + SliverToBoxAdapter( + child: Container( + height: 50, + alignment: Alignment.centerRight, + child: Wrap( + spacing: 8.0, + children: [ + ChoiceChip( + label: Text('New'), + selected: _filterChoice == 1, + onSelected: (bool selected) { + _filterChoice = 1; + _pagingController.refresh(); + }, + ), + ChoiceChip( + label: Text('Old'), + selected: _filterChoice == 2, + onSelected: (bool selected) { + _filterChoice = 2; + _pagingController.refresh(); + }, + ), + ChoiceChip( + label: Text('All'), + selected: _filterChoice == 3, + onSelected: (bool selected) { + _filterChoice = 3; + _pagingController.refresh(); + }, + ), + ], + ), + ), + ), + PagedSliverList( + state: state, + fetchNextPage: fetchNextPage, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + return Column( + children: [ + Dismissible( + key: UniqueKey(), + onDismissed: (direction) { + setState(() { + // _events!.removeAt(index); + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Event marked as read'), + behavior: SnackBarBehavior.floating, + showCloseIcon: true, + ), + ); + }, + background: Container( + color: Theme.of( + context, + ).colorScheme.primaryContainer, + alignment: Alignment.center, + child: Icon(Icons.done), + ), + child: ListTile( + // tileColor: item.isNew ? Colors.amber : null, + leading: Icon(item.notificationType.icon), + title: Text( + '${FriendlyDateFormatter().format(item.createdOn)} - ${item.title}', + ), + subtitle: Text(item.body, maxLines: 5), + trailing: Icon(Icons.more_vert), + onTap: () {}, + isThreeLine: true, + ), + ), + Divider(), + ], + ); + }, + ), + ), + ], + ), + ), + ); + // Paginated list end + } + @override void dispose() { _pagingController.dispose(); super.dispose(); } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Notifications')), - body: PagingListener( - controller: _pagingController, - builder: (context, state, fetchNextPage) => - PagedListView( - state: state, - fetchNextPage: fetchNextPage, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) { - return Dismissible( - key: UniqueKey(), - onDismissed: (direction) { - setState(() { - // _events!.removeAt(index); - }); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Event marked as read'), - behavior: SnackBarBehavior.floating, - showCloseIcon: true, - ), - ); - }, - background: Container( - color: Theme.of(context).colorScheme.primaryContainer, - alignment: Alignment.center, - child: Icon(Icons.done), - ), - child: ListTile( - leading: Icon(item.notificationType.icon), - title: Text( - '${FriendlyDateFormatter().format(item.createdOn)} - ${item.title}', - ), - subtitle: Text(item.body), - trailing: Icon(Icons.more_vert), - onTap: () {}, - ), - ); - }, - ), - ), - ), - ); - } } diff --git a/frontend/lib/utils/oott_api.dart b/frontend/lib/utils/oott_api.dart index 35808ef..d50435d 100644 --- a/frontend/lib/utils/oott_api.dart +++ b/frontend/lib/utils/oott_api.dart @@ -66,13 +66,17 @@ class BackendAPI { late String _apiKey; late Dio _dio; - Future> listNotifications(int offset) async { + Future> listNotifications(bool? isNew, int offset) async { print('About to call /notifications'); Response response; response = await _dio.get( '/notifications', - queryParameters: {'page_offset': offset, 'page_limit': _pageSize}, + queryParameters: { + 'is_new': isNew ?? '', + 'page_offset': offset, + 'page_limit': _pageSize, + }, ); print('Received: ' + response.data.toString());