From 61f2579152a11baec561b343251e09f1863921e9 Mon Sep 17 00:00:00 2001 From: rzuasti Date: Sun, 31 May 2026 10:28:23 -0400 Subject: [PATCH] Make notification mark-as-read reliably update the list Three intertwined fixes so that marking a notification as read from the trailing menu, swipe-dismiss, and 'Mark all as read' all consistently update the UI and surface backend failures. - Guard the mark-read, mark-new, and mark-all calls with try/catch; on failure show a snackbar with the mapped error and leave the list untouched. - Use the State's own `mounted` and `context` after the await instead of the per-card BuildContext, so the SliverList rebuild reliably runs even when the originating NotificationCard's element has been swapped out by the reconciliation. _setRead and _markAllAsRead no longer take a BuildContext. - Replace `Dismissible(key: UniqueKey())` with `ValueKey(item.id)` in the notification card. UniqueKey was regenerated on every build, forcing the Dismissible/PopupMenuButton subtree to be disposed on any parent rebuild; the open menu's `mounted` check would then drop `onSelected` silently. - Add `ValueKey(device.macAddress)` to the device list rows for the same reason, anchoring each row's Element (and its PopupMenuButton State) to its device identity instead of its list position. Co-Authored-By: Claude Opus 4.7 --- frontend/lib/devices/device_list.dart | 2 + frontend/lib/home/notification_card.dart | 17 ++++---- frontend/lib/home/notifications_list.dart | 51 +++++++++++++---------- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/frontend/lib/devices/device_list.dart b/frontend/lib/devices/device_list.dart index 76d528a..d3845d5 100644 --- a/frontend/lib/devices/device_list.dart +++ b/frontend/lib/devices/device_list.dart @@ -286,11 +286,13 @@ class _DeviceListState extends State with RouteAware { final device = _devices[index]; return isWide ? DeviceRowWide( + key: ValueKey(device.macAddress), device: device, formatter: formatter, onRefresh: () => _fetchPage(_currentPage), ) : DeviceRowCompact( + key: ValueKey(device.macAddress), device: device, formatter: formatter, onRefresh: () => _fetchPage(_currentPage), diff --git a/frontend/lib/home/notification_card.dart b/frontend/lib/home/notification_card.dart index 3e179e6..13608a1 100644 --- a/frontend/lib/home/notification_card.dart +++ b/frontend/lib/home/notification_card.dart @@ -7,7 +7,7 @@ import '../utils/friendly_date_formatter.dart'; class NotificationCard extends StatelessWidget { final oott_model.Notification item; final FriendlyDateFormatter formatter; - final Future Function(BuildContext, bool read) onSetRead; + final Future Function(bool read) onSetRead; const NotificationCard({ required this.item, @@ -22,10 +22,9 @@ class NotificationCard extends StatelessWidget { return Card( color: item.isNew ? theme.colorScheme.secondaryContainer : null, child: Dismissible( - key: UniqueKey(), - confirmDismiss: (direction) => direction == DismissDirection.startToEnd - ? onSetRead(context, false) - : onSetRead(context, true), + key: ValueKey(item.id), + confirmDismiss: (direction) => + onSetRead(direction != DismissDirection.startToEnd), background: Container( color: theme.colorScheme.tertiaryContainer, alignment: Alignment.centerLeft, @@ -54,14 +53,14 @@ class NotificationCard extends StatelessWidget { icon: const Icon(Icons.more_vert), onSelected: (value) async { if (value == 'view_device') { - if (item.isNew) await onSetRead(context, true); + if (item.isNew) await onSetRead(true); if (context.mounted) { context.push('/devices/${item.macAddress}'); } } else if (value == 'mark_read') { - await onSetRead(context, true); + await onSetRead(true); } else if (value == 'mark_new') { - await onSetRead(context, false); + await onSetRead(false); } }, itemBuilder: (context) => [ @@ -84,7 +83,7 @@ class NotificationCard extends StatelessWidget { ), onTap: item.macAddress != null ? () async { - if (item.isNew) await onSetRead(context, true); + if (item.isNew) await onSetRead(true); if (context.mounted) { context.push('/devices/${item.macAddress}'); } diff --git a/frontend/lib/home/notifications_list.dart b/frontend/lib/home/notifications_list.dart index 19dd5cd..77ea608 100644 --- a/frontend/lib/home/notifications_list.dart +++ b/frontend/lib/home/notifications_list.dart @@ -112,32 +112,41 @@ class _NotificationsListState extends State with RouteAware { } } - Future _markAllAsRead(BuildContext context) async { - await BackendAPI.instance.markAllNotificationsAsRead(); - _fetchPage(_currentPage); - if (context.mounted) { - UISnackbars.showSuccess(context, 'All notifications marked as read'); + Future _markAllAsRead() async { + try { + await BackendAPI.instance.markAllNotificationsAsRead(); + } catch (e) { + if (!mounted) return; + UISnackbars.showError(context, dioErrorToUserMessage(e)); + return; } + if (!mounted) return; + _fetchPage(_currentPage); + UISnackbars.showSuccess(context, 'All notifications marked as read'); } - Future _setRead( - BuildContext context, - oott_model.Notification item, - bool read, - ) async { + Future _setRead(oott_model.Notification item, bool read) async { if (item.isNew == !read) { - UISnackbars.showWarning( - context, - 'Notification was already marked as ${read ? 'read' : 'unread'}', - ); + if (mounted) { + UISnackbars.showWarning( + context, + 'Notification was already marked as ${read ? 'read' : 'unread'}', + ); + } return false; } - if (read) { - await BackendAPI.instance.markNotificationAsRead(item.id); - } else { - await BackendAPI.instance.markNotificationAsNew(item.id); + try { + if (read) { + await BackendAPI.instance.markNotificationAsRead(item.id); + } else { + await BackendAPI.instance.markNotificationAsNew(item.id); + } + } catch (e) { + if (!mounted) return false; + UISnackbars.showError(context, dioErrorToUserMessage(e)); + return false; } - if (!context.mounted) return false; + if (!mounted) return false; UISnackbars.showSuccess( context, 'Event marked as ${read ? 'read' : 'unread'}', @@ -185,7 +194,7 @@ class _NotificationsListState extends State with RouteAware { const Spacer(), if (_filter == _NotificationFilter.newOnly && _items.isNotEmpty) IconButton( - onPressed: () => _markAllAsRead(context), + onPressed: _markAllAsRead, icon: const Icon(Icons.done_all), tooltip: 'Mark all as read', ), @@ -266,7 +275,7 @@ class _NotificationsListState extends State with RouteAware { return NotificationCard( item: item, formatter: formatter, - onSetRead: (ctx, read) => _setRead(ctx, item, read), + onSetRead: (read) => _setRead(item, read), ); }, );