From 1e97ba98316a3098eb10507c7d970060653b2651 Mon Sep 17 00:00:00 2001 From: Janez T Date: Tue, 17 Mar 2026 09:44:07 +0100 Subject: [PATCH] feat: Favourites support using firmware flags bit 0 - Contact.isFavourite reads bit 0 of firmware flags field - Toggle favourite via contact action sheet (writes to device) - Favourites section shown first in contacts tab - Path size options corrected to [1, 2, 3] bytes --- lib/providers/contacts_provider.dart | 2 ++ lib/screens/contacts_tab.dart | 13 +++++++++ lib/services/contact_storage_service.dart | 32 +++++++++++++++++++++++ lib/services/route_hash_preferences.dart | 3 +-- lib/widgets/contacts/contact_tile.dart | 23 ++++++++++++++++ pubspec.yaml | 2 +- 6 files changed, 72 insertions(+), 3 deletions(-) diff --git a/lib/providers/contacts_provider.dart b/lib/providers/contacts_provider.dart index 455d3f2..1873964 100644 --- a/lib/providers/contacts_provider.dart +++ b/lib/providers/contacts_provider.dart @@ -342,6 +342,8 @@ class ContactsProvider with ChangeNotifier { } List get contacts => _contacts.values.toList(); + List get favouriteContacts => + _contacts.values.where((c) => c.isFavourite).toList(); List get savedContactGroups => List.from(_savedContactGroups) ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); diff --git a/lib/screens/contacts_tab.dart b/lib/screens/contacts_tab.dart index 666b7f5..bab45c1 100644 --- a/lib/screens/contacts_tab.dart +++ b/lib/screens/contacts_tab.dart @@ -688,6 +688,19 @@ class _ContactsTabState extends State { child: ListView( padding: const EdgeInsets.all(8), children: [ + // Favourites (contacts with firmware favourite flag set) + if (contactsProvider.favouriteContacts.isNotEmpty) ...[ + _SectionHeader( + title: 'Favourites', + count: contactsProvider.favouriteContacts.length, + icon: Icons.star, + ), + ..._buildContactSectionItems( + contactsProvider.favouriteContacts, + ), + const Divider(height: 32), + ], + // Team Members (Chat contacts) if (showTeamMembersSection) ...[ _SectionHeader( diff --git a/lib/services/contact_storage_service.dart b/lib/services/contact_storage_service.dart index 98f8e70..b71fdd3 100644 --- a/lib/services/contact_storage_service.dart +++ b/lib/services/contact_storage_service.dart @@ -210,6 +210,38 @@ class ContactStorageService { } } + // ── Favorites ─────────────────────────────────────────────────────────────── + + static const String _favoritesKey = 'contact_favorites'; + + Future saveFavorites( + Set publicKeyHexes, { + String? namespace, + }) async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.setStringList( + _key(_favoritesKey, namespace: namespace), + publicKeyHexes.toList(), + ); + } catch (e) { + debugPrint('❌ [ContactStorage] Error saving favorites: $e'); + } + } + + Future> loadFavorites({String? namespace}) async { + try { + final prefs = await SharedPreferences.getInstance(); + final list = prefs.getStringList( + _key(_favoritesKey, namespace: namespace), + ); + return list?.toSet() ?? {}; + } catch (e) { + debugPrint('❌ [ContactStorage] Error loading favorites: $e'); + return {}; + } + } + /// Get storage statistics Future> getStorageStats({String? namespace}) async { try { diff --git a/lib/services/route_hash_preferences.dart b/lib/services/route_hash_preferences.dart index d99e9ce..0a92afc 100644 --- a/lib/services/route_hash_preferences.dart +++ b/lib/services/route_hash_preferences.dart @@ -25,8 +25,7 @@ class RouteHashPreferences { /// Valid hash sizes per the MeshCore protocol. /// The path encoding uses 2 bits: 00=1B, 01=2B, 10=3B, 11=4B. - /// The official app offers 1, 2, 4 (skipping 3). - static const List supportedSizes = [1, 2, 4]; + static const List supportedSizes = [1, 2, 3]; static int _normalize(int value) { if (supportedSizes.contains(value)) return value; diff --git a/lib/widgets/contacts/contact_tile.dart b/lib/widgets/contacts/contact_tile.dart index c192e59..2d124b2 100644 --- a/lib/widgets/contacts/contact_tile.dart +++ b/lib/widgets/contacts/contact_tile.dart @@ -348,6 +348,29 @@ class ContactTile extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ + if (!contact.isChannel) + ListTile( + leading: Icon( + contact.isFavourite ? Icons.star : Icons.star_outline, + color: contact.isFavourite ? Colors.amber : null, + ), + title: Text( + contact.isFavourite + ? 'Remove from Favourites' + : 'Add to Favourites', + ), + onTap: () async { + Navigator.pop(sheetContext); + final toggled = contact.toggleFavourite(); + final connectionProvider = + context.read(); + await connectionProvider.addOrUpdateContact(toggled); + if (context.mounted) { + // Refresh contact from device so the local cache is updated + await connectionProvider.getContact(contact.publicKey); + } + }, + ), ListTile( leading: const Icon(Icons.message_outlined), title: Text(l10n.messages), diff --git a/pubspec.yaml b/pubspec.yaml index 47cf7ce..6003734 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,7 +44,7 @@ dependencies: meshcore_client: git: url: https://github.com/dz0ny/meshcore_client.git - ref: "77c1f81f24b4839cf70439a478ff077c026c120e" + ref: "b8d7549709c02a99dbde9e77a19fb65227d7e2c2" # Codec2 ultra-low-bitrate speech codec (FFI plugin) codec2_flutter: