Add mark as read action to notifications list

Users can now mark a notification as read via a popup menu on each list
item or by swiping left. Both actions call the backend API, remove the
item from the list, and show a snackbar confirmation. Swiping an already
read notification shows an error snackbar and bounces the item back.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-05-27 09:58:39 -04:00
co-authored by Claude Sonnet 4.6
parent 09b67c2707
commit 913bf0e839
4 changed files with 62 additions and 21 deletions
+8 -3
View File
@@ -19,9 +19,9 @@ For Flutter/Dart code:
## Project commands ## Project commands
- `` - Run the backend - `sudo cargo run` from the `backend/` folder - Run the backend (you need sudo to enable the process to attach itself to the network channel)
- `` - Run the front-end for the web - `flutter run -d web-server --web-port 3333` from the `frontend/` folder - Run the front-end for the web
- `` - Run the backend tests - `run_tests.sh` - Run the backend tests
## Architecture ## Architecture
@@ -40,5 +40,10 @@ For Flutter/Dart code:
- It uses the [Material 3](https://m3.material.io/develop/flutter) framework for Flutter - It uses the [Material 3](https://m3.material.io/develop/flutter) framework for Flutter
- The application needs to be responsive and adapt to a web experience in the desktop, tablets and phones - The application needs to be responsive and adapt to a web experience in the desktop, tablets and phones
- The application is also available in iOS and Android as a native experience (via de App Store and Play store) - The application is also available in iOS and Android as a native experience (via de App Store and Play store)
- Backend API access is implemented in the `utils/oott_api.dart` component
## Important notes ## Important notes
- NEVER add or commit .env files or files with secrets (passwords, API keys or similar information)
- Code must be as simple as possible, human readable and modularized
- ALWAYS write unit tests for new or modified backend components
- ALWAYS run all tests after making a new change and do not continue until all tests pass
@@ -32,6 +32,34 @@ class _NotificationListState extends State<NotificationList> {
}, },
); );
Future<bool> _markAsRead(
BuildContext context,
oott_model.Notification item,
) async {
if (!item.isNew) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Notification was already marked as read'),
behavior: SnackBarBehavior.floating,
showCloseIcon: true,
),
);
return false;
}
await BackendAPI.instance.markNotificationAsRead(item.id);
_pagingController.value = _pagingController.value.filterItems(
(n) => n.id != item.id,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Event marked as read'),
behavior: SnackBarBehavior.floating,
showCloseIcon: true,
),
);
return true;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -87,19 +115,8 @@ class _NotificationListState extends State<NotificationList> {
children: [ children: [
Dismissible( Dismissible(
key: UniqueKey(), key: UniqueKey(),
onDismissed: (direction) { confirmDismiss: (direction) =>
setState(() { _markAsRead(context, item),
// _events!.removeAt(index);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Event marked as read'),
behavior: SnackBarBehavior.floating,
showCloseIcon: true,
),
);
},
background: Container( background: Container(
color: Theme.of( color: Theme.of(
context, context,
@@ -114,7 +131,21 @@ class _NotificationListState extends State<NotificationList> {
'${FriendlyDateFormatter().format(item.createdOn)} - ${item.title}', '${FriendlyDateFormatter().format(item.createdOn)} - ${item.title}',
), ),
subtitle: Text(item.body, maxLines: 5), subtitle: Text(item.body, maxLines: 5),
trailing: Icon(Icons.more_vert), trailing: PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) async {
if (value == 'mark_read') {
await _markAsRead(context, item);
}
},
itemBuilder: (context) => [
if (item.isNew)
const PopupMenuItem(
value: 'mark_read',
child: Text('Mark as read'),
),
],
),
onTap: () {}, onTap: () {},
isThreeLine: true, isThreeLine: true,
), ),
+5
View File
@@ -66,6 +66,11 @@ class BackendAPI {
late String _apiKey; late String _apiKey;
late Dio _dio; late Dio _dio;
Future<void> markNotificationAsRead(int id) async {
print('About to call /notifications/$id');
await _dio.get('/notifications/$id');
}
Future<List<Notification>> listNotifications(bool? isNew, int offset) async { Future<List<Notification>> listNotifications(bool? isNew, int offset) async {
print('About to call /notifications'); print('About to call /notifications');
+4 -4
View File
@@ -244,10 +244,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.18" version: "0.12.19"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
@@ -457,10 +457,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.9" version: "0.7.10"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description: