From 566e3aadf83d9124007014617cc5e36e8679ee50 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Mar 2026 17:59:48 -0700 Subject: [PATCH] fix: migrate filter menus to type-safe generics and harden popup dismissal - Move ContactSortOption/ContactTypeFilter enums to dedicated contact_filter_types.dart (re-exported from contact_search.dart) - Migrate ContactsFilterMenu and DiscoveryContactsFilterMenu to use sealed class action types with SortFilterMenu generics, replacing int action constants and switch statements - Guard _closeDropdownAndRun with ModalRoute.isCurrent check to prevent accidental dismissal of parent routes Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/screens/contacts_screen.dart | 7 +- lib/utils/contact_filter_types.dart | 3 + lib/utils/contact_search.dart | 4 +- lib/widgets/list_filter_widget.dart | 130 ++++++++++++---------------- 4 files changed, 66 insertions(+), 78 deletions(-) create mode 100644 lib/utils/contact_filter_types.dart diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index abb29fad..a6739e1d 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -454,8 +454,11 @@ class _ContactsScreenState extends State } } - void _closeDropdownAndRun(BuildContext context, VoidCallback action) { - Navigator.of(context).pop(); + void _closeDropdownAndRun(BuildContext popupContext, VoidCallback action) { + final route = ModalRoute.of(popupContext); + if (route != null && route.isCurrent) { + Navigator.of(popupContext).pop(); + } WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; action(); diff --git a/lib/utils/contact_filter_types.dart b/lib/utils/contact_filter_types.dart new file mode 100644 index 00000000..08e07c25 --- /dev/null +++ b/lib/utils/contact_filter_types.dart @@ -0,0 +1,3 @@ +enum ContactSortOption { lastSeen, recentMessages, name } + +enum ContactTypeFilter { all, favorites, users, repeaters, rooms } diff --git a/lib/utils/contact_search.dart b/lib/utils/contact_search.dart index 849172a4..7a82c53a 100644 --- a/lib/utils/contact_search.dart +++ b/lib/utils/contact_search.dart @@ -1,8 +1,6 @@ import '../models/contact.dart'; -enum ContactSortOption { lastSeen, recentMessages, name } - -enum ContactTypeFilter { all, favorites, users, repeaters, rooms } +export 'contact_filter_types.dart'; bool matchesContactQuery(Contact contact, String query) { final normalizedQuery = query.trim().toLowerCase(); diff --git a/lib/widgets/list_filter_widget.dart b/lib/widgets/list_filter_widget.dart index 8b2874b9..c4fd5aa7 100644 --- a/lib/widgets/list_filter_widget.dart +++ b/lib/widgets/list_filter_widget.dart @@ -87,15 +87,23 @@ class SortFilterMenu extends StatelessWidget { } } -const int _actionSortRecentMessages = 1; -const int _actionSortName = 2; -const int _actionSortLastSeen = 3; -const int _actionFilterAll = 4; -const int _actionFilterFavorites = 5; -const int _actionFilterUsers = 6; -const int _actionFilterRepeaters = 7; -const int _actionFilterRooms = 8; -const int _actionToggleUnreadOnly = 9; +sealed class _ContactsFilterAction { + const _ContactsFilterAction(); +} + +class _SortAction extends _ContactsFilterAction { + final ContactSortOption option; + const _SortAction(this.option); +} + +class _TypeFilterAction extends _ContactsFilterAction { + final ContactTypeFilter filter; + const _TypeFilterAction(this.filter); +} + +class _ToggleUnreadAction extends _ContactsFilterAction { + const _ToggleUnreadAction(); +} class ContactsFilterMenu extends StatelessWidget { final ContactSortOption sortOption; @@ -118,24 +126,24 @@ class ContactsFilterMenu extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - return SortFilterMenu( + return SortFilterMenu<_ContactsFilterAction>( tooltip: l10n.listFilter_tooltip, sections: [ SortFilterMenuSection( title: l10n.listFilter_sortBy, options: [ SortFilterMenuOption( - value: _actionSortRecentMessages, + value: _SortAction(ContactSortOption.recentMessages), label: l10n.listFilter_latestMessages, checked: sortOption == ContactSortOption.recentMessages, ), SortFilterMenuOption( - value: _actionSortLastSeen, + value: _SortAction(ContactSortOption.lastSeen), label: l10n.listFilter_heardRecently, checked: sortOption == ContactSortOption.lastSeen, ), SortFilterMenuOption( - value: _actionSortName, + value: _SortAction(ContactSortOption.name), label: l10n.listFilter_az, checked: sortOption == ContactSortOption.name, ), @@ -145,32 +153,32 @@ class ContactsFilterMenu extends StatelessWidget { title: l10n.listFilter_filters, options: [ SortFilterMenuOption( - value: _actionFilterAll, + value: _TypeFilterAction(ContactTypeFilter.all), label: l10n.listFilter_all, checked: typeFilter == ContactTypeFilter.all, ), SortFilterMenuOption( - value: _actionFilterFavorites, + value: _TypeFilterAction(ContactTypeFilter.favorites), label: l10n.listFilter_favorites, checked: typeFilter == ContactTypeFilter.favorites, ), SortFilterMenuOption( - value: _actionFilterUsers, + value: _TypeFilterAction(ContactTypeFilter.users), label: l10n.listFilter_users, checked: typeFilter == ContactTypeFilter.users, ), SortFilterMenuOption( - value: _actionFilterRepeaters, + value: _TypeFilterAction(ContactTypeFilter.repeaters), label: l10n.listFilter_repeaters, checked: typeFilter == ContactTypeFilter.repeaters, ), SortFilterMenuOption( - value: _actionFilterRooms, + value: _TypeFilterAction(ContactTypeFilter.rooms), label: l10n.listFilter_roomServers, checked: typeFilter == ContactTypeFilter.rooms, ), SortFilterMenuOption( - value: _actionToggleUnreadOnly, + value: const _ToggleUnreadAction(), label: l10n.listFilter_unreadOnly, checked: showUnreadOnly, ), @@ -179,39 +187,32 @@ class ContactsFilterMenu extends StatelessWidget { ], onSelected: (action) { switch (action) { - case _actionSortRecentMessages: - onSortChanged(ContactSortOption.recentMessages); - break; - case _actionSortName: - onSortChanged(ContactSortOption.name); - break; - case _actionSortLastSeen: - onSortChanged(ContactSortOption.lastSeen); - break; - case _actionFilterAll: - onTypeFilterChanged(ContactTypeFilter.all); - break; - case _actionFilterUsers: - onTypeFilterChanged(ContactTypeFilter.users); - break; - case _actionFilterFavorites: - onTypeFilterChanged(ContactTypeFilter.favorites); - break; - case _actionFilterRepeaters: - onTypeFilterChanged(ContactTypeFilter.repeaters); - break; - case _actionFilterRooms: - onTypeFilterChanged(ContactTypeFilter.rooms); - break; - case _actionToggleUnreadOnly: + case _SortAction(:final option): + onSortChanged(option); + case _TypeFilterAction(:final filter): + onTypeFilterChanged(filter); + case _ToggleUnreadAction(): onUnreadOnlyChanged(!showUnreadOnly); - break; } }, ); } } +sealed class _DiscoveryFilterAction { + const _DiscoveryFilterAction(); +} + +class _DiscoverySortAction extends _DiscoveryFilterAction { + final ContactSortOption option; + const _DiscoverySortAction(this.option); +} + +class _DiscoveryTypeFilterAction extends _DiscoveryFilterAction { + final ContactTypeFilter filter; + const _DiscoveryTypeFilterAction(this.filter); +} + class DiscoveryContactsFilterMenu extends StatelessWidget { final ContactSortOption sortOption; final ContactTypeFilter typeFilter; @@ -229,19 +230,19 @@ class DiscoveryContactsFilterMenu extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - return SortFilterMenu( + return SortFilterMenu<_DiscoveryFilterAction>( tooltip: l10n.listFilter_tooltip, sections: [ SortFilterMenuSection( title: l10n.listFilter_sortBy, options: [ SortFilterMenuOption( - value: _actionSortLastSeen, + value: _DiscoverySortAction(ContactSortOption.lastSeen), label: l10n.listFilter_heardRecently, checked: sortOption == ContactSortOption.lastSeen, ), SortFilterMenuOption( - value: _actionSortName, + value: _DiscoverySortAction(ContactSortOption.name), label: l10n.listFilter_az, checked: sortOption == ContactSortOption.name, ), @@ -251,22 +252,22 @@ class DiscoveryContactsFilterMenu extends StatelessWidget { title: l10n.listFilter_filters, options: [ SortFilterMenuOption( - value: _actionFilterAll, + value: _DiscoveryTypeFilterAction(ContactTypeFilter.all), label: l10n.listFilter_all, checked: typeFilter == ContactTypeFilter.all, ), SortFilterMenuOption( - value: _actionFilterUsers, + value: _DiscoveryTypeFilterAction(ContactTypeFilter.users), label: l10n.listFilter_users, checked: typeFilter == ContactTypeFilter.users, ), SortFilterMenuOption( - value: _actionFilterRepeaters, + value: _DiscoveryTypeFilterAction(ContactTypeFilter.repeaters), label: l10n.listFilter_repeaters, checked: typeFilter == ContactTypeFilter.repeaters, ), SortFilterMenuOption( - value: _actionFilterRooms, + value: _DiscoveryTypeFilterAction(ContactTypeFilter.rooms), label: l10n.listFilter_roomServers, checked: typeFilter == ContactTypeFilter.rooms, ), @@ -275,27 +276,10 @@ class DiscoveryContactsFilterMenu extends StatelessWidget { ], onSelected: (action) { switch (action) { - case _actionSortName: - onSortChanged(ContactSortOption.name); - break; - case _actionSortLastSeen: - onSortChanged(ContactSortOption.lastSeen); - break; - case _actionFilterAll: - onTypeFilterChanged(ContactTypeFilter.all); - break; - case _actionFilterUsers: - onTypeFilterChanged(ContactTypeFilter.users); - break; - case _actionFilterFavorites: - onTypeFilterChanged(ContactTypeFilter.favorites); - break; - case _actionFilterRepeaters: - onTypeFilterChanged(ContactTypeFilter.repeaters); - break; - case _actionFilterRooms: - onTypeFilterChanged(ContactTypeFilter.rooms); - break; + case _DiscoverySortAction(:final option): + onSortChanged(option); + case _DiscoveryTypeFilterAction(:final filter): + onTypeFilterChanged(filter); } }, );