Dropdown Group Menu and filter persistance#292
Conversation
|
sadly I still have a problem that the groups are not saved reliably. |
There was a problem hiding this comment.
Pull request overview
Updates the Contacts and Channels screens to keep user-selected filters/search state across navigation, and changes contact group UX to use a dropdown “drill-in” style menu (per Issue #133).
Changes:
- Introduces
UiViewStateService(globalChangeNotifier) to persist contacts/channels view state (search text, sort/filter, selected group). - Reworks Contacts UI to select/manage groups via a dropdown menu (add/edit/delete actions embedded in the dropdown).
- Updates contact group storage key behavior and adds a legacy-key migration path.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| macos/Flutter/GeneratedPluginRegistrant.swift | Registers path_provider_foundation for macOS builds. |
| lib/widgets/list_filter_widget.dart | Removes “new group” action from the contacts filter menu. |
| lib/storage/contact_group_store.dart | Changes key scoping behavior and legacy migration logic for contact groups. |
| lib/services/ui_view_state_service.dart | Adds global view-state persistence for contacts/channels UI settings. |
| lib/screens/contacts_screen.dart | Implements group dropdown menu + moves filter/search state into UiViewStateService. |
| lib/screens/channels_screen.dart | Moves channel search/sort state into UiViewStateService. |
| lib/main.dart | Registers UiViewStateService in the app’s Provider tree. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| @@ -53,12 +50,13 @@ class ContactGroupStore { | |||
| } | |||
|
|
|||
| Future<void> saveGroups(List<ContactGroup> groups) async { | |||
| if (publicKeyHex.isEmpty) { | |||
| appLogger.warn('Public key hex is not set. Cannot save contact groups.'); | |||
| return; | |||
| } | |||
| final prefs = PrefsManager.instance; | |||
| final encoded = jsonEncode(groups.map((group) => group.toJson()).toList()); | |||
| if (publicKeyHex.isEmpty) { | |||
| appLogger.warn( | |||
| 'Public key hex is not set. Saving contact groups to unscoped key $_keyPrefix.', | |||
| ); | |||
| } | |||
| await prefs.setString(keyFor, encoded); | |||
| import 'package:flutter/foundation.dart'; | ||
|
|
||
| import '../widgets/list_filter_widget.dart'; | ||
|
|
||
| const contactsAllGroupsValue = '__all__'; | ||
|
|
||
| class UiViewStateService extends ChangeNotifier { | ||
| String _contactsSelectedGroupName = contactsAllGroupsValue; | ||
| String _contactsSearchText = ''; | ||
| bool _contactsSearchExpanded = false; | ||
| ContactSortOption _contactsSortOption = ContactSortOption.lastSeen; | ||
| bool _contactsShowUnreadOnly = false; | ||
| ContactTypeFilter _contactsTypeFilter = ContactTypeFilter.all; |
| final sortedGroupNames = _groups.map((group) => group.name).toSet().toList() | ||
| ..sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase())); | ||
|
|
||
| return Column( | ||
| children: [ | ||
| Padding( | ||
| padding: const EdgeInsets.all(8.0), | ||
| child: TextField( | ||
| controller: _searchController, | ||
| decoration: InputDecoration( | ||
| hintText: hintText, | ||
| prefixIcon: const Icon(Icons.search), | ||
| suffixIcon: Row( | ||
| mainAxisSize: MainAxisSize.min, | ||
| children: [ | ||
| if (_searchQuery.isNotEmpty) | ||
| IconButton( | ||
| icon: const Icon(Icons.clear), | ||
| onPressed: () { | ||
| _searchController.clear(); | ||
| setState(() { | ||
| _searchQuery = ''; | ||
| }); | ||
| }, | ||
| child: Row( | ||
| children: [ | ||
| Expanded( | ||
| child: DropdownButtonFormField<String>( | ||
| initialValue: _selectedGroup?.name ?? contactsAllGroupsValue, | ||
| decoration: const InputDecoration( | ||
| border: InputBorder.none, | ||
| contentPadding: EdgeInsets.symmetric( | ||
| horizontal: 12, | ||
| vertical: 10, | ||
| ), | ||
| _buildFilterButton(context, connector), | ||
| ], | ||
| ), | ||
| border: OutlineInputBorder( | ||
| borderRadius: BorderRadius.circular(12), | ||
| ), | ||
| items: [ | ||
| DropdownMenuItem<String>( | ||
| value: contactsAllGroupsValue, | ||
| child: Row( | ||
| mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||
| children: [ | ||
| Text(context.l10n.listFilter_all), | ||
| GestureDetector( | ||
| behavior: HitTestBehavior.opaque, | ||
| onTap: () => _closeDropdownAndRun( | ||
| context, | ||
| () => _showGroupEditor(context, contacts), | ||
| ), | ||
| child: const Icon(Icons.group_add, size: 20), | ||
| ), | ||
| ], | ||
| ), | ||
| ), | ||
| ...sortedGroupNames.map((name) { | ||
| final group = _groups.firstWhere((g) => g.name == name); | ||
| return DropdownMenuItem<String>( |
| DropdownMenuItem<String>( | ||
| value: contactsAllGroupsValue, | ||
| child: Row( | ||
| mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||
| children: [ | ||
| Text(context.l10n.listFilter_all), | ||
| GestureDetector( | ||
| behavior: HitTestBehavior.opaque, | ||
| onTap: () => _closeDropdownAndRun( | ||
| context, | ||
| () => _showGroupEditor(context, contacts), | ||
| ), | ||
| child: const Icon(Icons.group_add, size: 20), | ||
| ), | ||
| ], | ||
| ), | ||
| ), | ||
| ...sortedGroupNames.map((name) { | ||
| final group = _groups.firstWhere((g) => g.name == name); | ||
| return DropdownMenuItem<String>( | ||
| value: name, | ||
| child: Row( | ||
| mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||
| children: [ | ||
| Expanded( | ||
| child: Text( | ||
| name, | ||
| overflow: TextOverflow.ellipsis, | ||
| ), | ||
| ), | ||
| GestureDetector( | ||
| behavior: HitTestBehavior.opaque, | ||
| onTap: () => _closeDropdownAndRun( | ||
| context, | ||
| () => _showGroupEditor( | ||
| context, | ||
| contacts, | ||
| group: group, | ||
| ), | ||
| ), | ||
| child: const Icon(Icons.edit, size: 20), | ||
| ), | ||
| const SizedBox(width: 8), | ||
| GestureDetector( | ||
| behavior: HitTestBehavior.opaque, | ||
| onTap: () => _closeDropdownAndRun( | ||
| context, | ||
| () => _confirmDeleteGroup(context, group), | ||
| ), | ||
| child: const Icon( | ||
| Icons.delete, | ||
| size: 20, | ||
| color: Colors.red, | ||
| ), | ||
| ), |
There was a problem hiding this comment.
Pull request overview
This PR updates the Contacts (and Channels) UI to (1) preserve filter/sort/search state across navigation and (2) replace the previous group “popup members” flow with a dropdown group selector that drills into a group like a filtered contact list (per Issue #133).
Changes:
- Introduces
UiViewStateService(ProviderChangeNotifier) to hold Contacts/Channels view state (search text, sort, filters, selected group). - Refactors Contacts screen to use a group dropdown (create/edit/delete actions inside the menu) and applies selected-group filtering to the contact list.
- Refactors Channels screen to use shared view state for search and sort, and moves contact filter enums into
contact_search.dart.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| macos/Flutter/GeneratedPluginRegistrant.swift | Registers path_provider_foundation plugin for macOS build. |
| lib/widgets/list_filter_widget.dart | Removes locally-defined contact filter enums; now imports shared enums from contact_search.dart. |
| lib/utils/contact_search.dart | Becomes the shared home for ContactSortOption / ContactTypeFilter enums used across screens. |
| lib/services/ui_view_state_service.dart | Adds centralized in-memory UI state for Contacts/Channels to persist across navigation. |
| lib/screens/contacts_screen.dart | Uses UiViewStateService, adds group dropdown drill-in UI, and applies selected-group filtering. |
| lib/screens/channels_screen.dart | Uses UiViewStateService for persisted search/sort state and adds mapping for sort selection. |
| lib/main.dart | Registers UiViewStateService in the app-wide MultiProvider. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| switch (value) { | ||
| case 0: | ||
| return ChannelSortOption.manual; | ||
| case 2: | ||
| return ChannelSortOption.latestMessages; | ||
| case 3: | ||
| return ChannelSortOption.unread; | ||
| case 1: | ||
| default: | ||
| return ChannelSortOption.name; |
| onPressed: () { | ||
| if (viewState | ||
| .contactsSearchText | ||
| .isNotEmpty) { | ||
| _searchController.clear(); | ||
| viewState.setContactsSearchText(''); | ||
| return; |
| if (viewState.channelsSearchText.isNotEmpty) | ||
| IconButton( | ||
| icon: const Icon(Icons.clear), | ||
| onPressed: () { | ||
| _searchController.clear(); | ||
| setState(() { | ||
| _searchQuery = ''; | ||
| }); | ||
| context | ||
| .read<UiViewStateService>() | ||
| .setChannelsSearchText(''); | ||
| }, | ||
| ), |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the Contacts screen UX by replacing the “popup group members” flow with a dropdown group menu (per Issue #133) and introduces an app-scoped view state service so Contacts/Channels search & filter UI state persists across navigation.
Changes:
- Added
UiViewStateService(ChangeNotifier) to hold Contacts/Channels view state (search text, sort/filter selections, selected group). - Refactored Contacts screen to use the shared view state, add a dropdown group selector, and filter contacts by selected group.
- Refactored Channels screen to use the shared view state for search text and sort selection persistence.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| macos/Flutter/GeneratedPluginRegistrant.swift | Registers path_provider_foundation plugin for macOS build. |
| lib/widgets/list_filter_widget.dart | Moves contact sort/type enums out; removes “new group” action from the filter menu. |
| lib/utils/contact_search.dart | Centralizes ContactSortOption / ContactTypeFilter enums with contact query helpers. |
| lib/services/ui_view_state_service.dart | New global view-state service for persisting Contacts/Channels UI selections. |
| lib/screens/contacts_screen.dart | Implements dropdown group menu + wires Contacts filters/search/sort to UiViewStateService. |
| lib/screens/channels_screen.dart | Wires Channels search/sort to UiViewStateService. |
| lib/main.dart | Provides UiViewStateService at app level via Provider. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| return PopupMenuButton<String>( | ||
| position: PopupMenuPosition.under, | ||
| constraints: BoxConstraints.tightFor(width: menuWidth), | ||
| onSelected: (value) { | ||
| viewState.setContactsSelectedGroupName(value); | ||
| }, | ||
| itemBuilder: (context) => [ | ||
| PopupMenuItem<String>( | ||
| value: contactsAllGroupsValue, | ||
| child: Row( |
There was a problem hiding this comment.
Pull request overview
Updates the Contacts screen UX to support a drill-in style group dropdown (per Issue #133) and preserves Contacts/Channels filter/search state across navigation by lifting view state into a shared service.
Changes:
- Add
UiViewStateService(provided app-wide) to keep Contacts/Channels search + filter state stable across route changes. - Replace the Contacts “group popup” UX with a dropdown group selector that supports create/edit/delete actions.
- Move
ContactSortOption/ContactTypeFilterenums intocontact_search.dartfor reuse across widgets/services.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| macos/Flutter/GeneratedPluginRegistrant.swift | Registers path_provider_foundation plugin for macOS build. |
| lib/widgets/list_filter_widget.dart | Removes locally-defined contact filter enums and imports shared definitions. |
| lib/utils/contact_search.dart | Adds shared enums for contact sort and type filtering. |
| lib/services/ui_view_state_service.dart | New ChangeNotifier storing Contacts/Channels view state (search, sort, filters, selected group). |
| lib/screens/contacts_screen.dart | Refactors Contacts screen to use UiViewStateService and adds group dropdown UI + group-based filtering. |
| lib/screens/channels_screen.dart | Refactors Channels screen search/sort state to use UiViewStateService. |
| lib/main.dart | Wires UiViewStateService into the app’s MultiProvider. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| final selectedGroupName = | ||
| _selectedGroup?.name ?? context.l10n.listFilter_all; | ||
| final menuWidth = MediaQuery.sizeOf(context).width - 16; | ||
|
|
||
| return PopupMenuButton<String>( | ||
| position: PopupMenuPosition.under, | ||
| constraints: BoxConstraints.tightFor(width: menuWidth), | ||
| onSelected: (value) { |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the Contacts (and Channels) UI to keep user-selected search/sort/filter state when navigating between screens, and replaces the previous “group tiles / popup members” approach with a dropdown-based group selector (per Issue #133).
Changes:
- Introduces a shared
UiViewStateServiceto retain Contacts/Channels search and filter state across navigation. - Reworks Contacts groups UI into a dropdown selector with inline create/edit/delete actions.
- Wires the new view-state service into app startup and updates Channels/Contacts screens to use it.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| macos/Flutter/GeneratedPluginRegistrant.swift | Registers path_provider_foundation plugin for macOS. |
| lib/widgets/list_filter_widget.dart | Removes “new group” action from the filter menu and sources filter enums from a shared location. |
| lib/utils/contact_search.dart | Centralizes ContactSortOption / ContactTypeFilter enums alongside contact query matching utilities. |
| lib/services/ui_view_state_service.dart | Adds a ChangeNotifier holding Contacts/Channels view state. |
| lib/screens/contacts_screen.dart | Implements group dropdown UI and migrates contact filters/search/sort to UiViewStateService; scopes group load/save by node key. |
| lib/screens/channels_screen.dart | Migrates channel search + sort selection to UiViewStateService. |
| lib/main.dart | Creates and provides UiViewStateService via MultiProvider. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| Future<void> _saveGroups() async { | ||
| _groupStore.setPublicKeyHex = context | ||
| .read<MeshCoreConnector>() | ||
| .selfPublicKeyHex; | ||
| await _groupStore.saveGroups(_groups); | ||
| } |
| class UiViewStateService extends ChangeNotifier { | ||
| String? _contactsSelectedGroupName = contactsAllGroupsValue; | ||
| String _contactsSearchText = ''; | ||
| bool _contactsSearchExpanded = false; | ||
| ContactSortOption _contactsSortOption = ContactSortOption.lastSeen; | ||
| bool _contactsShowUnreadOnly = false; | ||
| ContactTypeFilter _contactsTypeFilter = ContactTypeFilter.all; | ||
|
|
||
| String _channelsSearchText = ''; | ||
| int _channelsSortIndex = 0; | ||
|
|
| final selectedGroupName = | ||
| _selectedGroup?.name ?? context.l10n.listFilter_all; | ||
| final menuWidth = MediaQuery.sizeOf(context).width - 16; | ||
|
|
||
| return PopupMenuButton<String?>( | ||
| position: PopupMenuPosition.under, | ||
| constraints: BoxConstraints.tightFor(width: menuWidth), |
| IconButton( | ||
| tooltip: context.l10n.contacts_newGroup, | ||
| constraints: const BoxConstraints(), | ||
| padding: EdgeInsets.zero, | ||
| icon: const Icon(Icons.group_add, size: 20), |
| switch (value) { | ||
| case 0: | ||
| return ChannelSortOption.manual; | ||
| case 2: | ||
| return ChannelSortOption.latestMessages; | ||
| case 3: | ||
| return ChannelSortOption.unread; | ||
| case 1: | ||
| default: | ||
| return ChannelSortOption.name; | ||
| } |
| Future<void> _loadGroups() async { | ||
| _groupStore.setPublicKeyHex = context | ||
| .read<MeshCoreConnector>() | ||
| .selfPublicKeyHex; | ||
| final groups = await _groupStore.loadGroups(); | ||
| if (!mounted) return; | ||
| setState(() { | ||
| _groups = groups; | ||
| _ensureValidSelectedGroup(); | ||
| }); |
|
Well it worked before I tried to fix all these codex issues... |
Code Review FindingsMust Fix
Should Fix
Nice to Have
|
There was a problem hiding this comment.
Pull request overview
Adds persistent UI state for Contacts/Channels and replaces the prior contact-group interaction with a dropdown “drill-in” style menu, addressing Issue #133 and preventing filter resets when navigating away from the screens.
Changes:
- Introduce
UiViewStateServiceto centralize and persist contact/channel sort + filter state viaPrefsManager. - Update Contacts screen to use a group dropdown selector and new collapsible search/filter UI.
- Update Channels screen to read/write sort + search state via
UiViewStateService; add a reserved group-name localization.
Reviewed changes
Copilot reviewed 38 out of 38 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| macos/Flutter/GeneratedPluginRegistrant.swift | Registers path_provider_foundation plugin on macOS. |
| lib/widgets/list_filter_widget.dart | Moves contact filter enums out; simplifies contacts filter menu (removes “New Group” action). |
| lib/utils/contact_search.dart | Hosts ContactSortOption / ContactTypeFilter enums alongside contact query helpers. |
| lib/services/ui_view_state_service.dart | New persisted UI state holder for contacts/channels filters + sorts. |
| lib/screens/contacts_screen.dart | Implements group dropdown menu + uses shared view state for filters/search. |
| lib/screens/channels_screen.dart | Uses shared view state for search + sort persistence. |
| lib/main.dart | Initializes/provides UiViewStateService app-wide. |
| lib/l10n/app_en.arb | Adds contacts_groupNameReserved string. |
| lib/l10n/app_de.arb | Adds contacts_groupNameReserved string. |
| lib/l10n/app_es.arb | Adds contacts_groupNameReserved string. |
| lib/l10n/app_fr.arb | Adds contacts_groupNameReserved string. |
| lib/l10n/app_it.arb | Adds contacts_groupNameReserved string. |
| lib/l10n/app_nl.arb | Adds contacts_groupNameReserved string. |
| lib/l10n/app_pl.arb | Adds contacts_groupNameReserved string. |
| lib/l10n/app_pt.arb | Adds contacts_groupNameReserved string. |
| lib/l10n/app_ru.arb | Adds contacts_groupNameReserved string. |
| lib/l10n/app_sk.arb | Adds contacts_groupNameReserved string. |
| lib/l10n/app_sl.arb | Adds contacts_groupNameReserved string. |
| lib/l10n/app_sv.arb | Adds contacts_groupNameReserved string. |
| lib/l10n/app_uk.arb | Adds contacts_groupNameReserved string. |
| lib/l10n/app_zh.arb | Adds contacts_groupNameReserved string. |
| lib/l10n/app_bg.arb | Adds contacts_groupNameReserved string. |
| lib/l10n/app_localizations.dart | Adds accessor for contacts_groupNameReserved. |
| lib/l10n/app_localizations_en.dart | Implements contacts_groupNameReserved. |
| lib/l10n/app_localizations_de.dart | Implements contacts_groupNameReserved. |
| lib/l10n/app_localizations_es.dart | Implements contacts_groupNameReserved. |
| lib/l10n/app_localizations_fr.dart | Implements contacts_groupNameReserved. |
| lib/l10n/app_localizations_it.dart | Implements contacts_groupNameReserved. |
| lib/l10n/app_localizations_nl.dart | Implements contacts_groupNameReserved. |
| lib/l10n/app_localizations_pl.dart | Implements contacts_groupNameReserved. |
| lib/l10n/app_localizations_pt.dart | Implements contacts_groupNameReserved. |
| lib/l10n/app_localizations_ru.dart | Implements contacts_groupNameReserved. |
| lib/l10n/app_localizations_sk.dart | Implements contacts_groupNameReserved. |
| lib/l10n/app_localizations_sl.dart | Implements contacts_groupNameReserved. |
| lib/l10n/app_localizations_sv.dart | Implements contacts_groupNameReserved. |
| lib/l10n/app_localizations_uk.dart | Implements contacts_groupNameReserved. |
| lib/l10n/app_localizations_zh.dart | Implements contacts_groupNameReserved. |
| lib/l10n/app_localizations_bg.dart | Implements contacts_groupNameReserved. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| void setContactsSortOption(ContactSortOption value) { | ||
| if (_contactsSortOption == value) return; | ||
| _contactsSortOption = value; | ||
| notifyListeners(); | ||
| PrefsManager.instance.setString(_keyContactsSortOption, value.name); | ||
| } | ||
|
|
||
| void setContactsShowUnreadOnly(bool value) { | ||
| if (_contactsShowUnreadOnly == value) return; | ||
| _contactsShowUnreadOnly = value; | ||
| notifyListeners(); | ||
| PrefsManager.instance.setBool(_keyContactsShowUnreadOnly, value); | ||
| } | ||
|
|
||
| void setContactsTypeFilter(ContactTypeFilter value) { | ||
| if (_contactsTypeFilter == value) return; | ||
| _contactsTypeFilter = value; | ||
| notifyListeners(); | ||
| PrefsManager.instance.setString(_keyContactsTypeFilter, value.name); | ||
| } |
| if (name.toLowerCase() == | ||
| contactsAllGroupsValue.toLowerCase()) { | ||
| ScaffoldMessenger.of(context).showSnackBar( | ||
| SnackBar( | ||
| content: Text(context.l10n.contacts_groupNameReserved), | ||
| ), | ||
| ); | ||
| return; | ||
| } |
There was a problem hiding this comment.
The last time he said I should not make it nullable! And her he wants null! What should I do?
| if (viewState.contactsSearchText.isEmpty) return true; | ||
| return matchesContactQuery( | ||
| contact, | ||
| viewState.contactsSearchText.toLowerCase(), | ||
| ); |

Pull Request for changes on Contact screen.
Filter persistance:
In this Pull-Request I want to make changes, that the set Filters will be saved.
With that, pagechanges to/from contacts, channels, map or opening a chat will not reset the filters set.
Dropdown Group Menu, like proposed in Issue #133.