From 723bf7293c0765510096dbf0a29a324493d62f0d Mon Sep 17 00:00:00 2001 From: ericz Date: Tue, 17 Mar 2026 21:56:42 +0100 Subject: [PATCH 1/6] location aware channel_message_path --- .gitignore | 3 +- lib/screens/channel_message_path_screen.dart | 93 +++++++++++--------- 2 files changed, 54 insertions(+), 42 deletions(-) diff --git a/.gitignore b/.gitignore index a312737..779856c 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,7 @@ secrets.dart .DS_Store .AppleDouble .LSOverride +macos/Flutter/GeneratedPluginRegistrant.swift # iOS **/ios/Pods/ @@ -85,4 +86,4 @@ keystore.properties .vscode/settings.json # Cloudflare Wrangler -.wrangler \ No newline at end of file +.wrangler diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 747c2bf..e2c5b49 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -40,8 +40,7 @@ class ChannelMessagePathScreen extends StatelessWidget { final primaryPath = !channelMessage && !message.isOutgoing ? Uint8List.fromList(primaryPathTmp.reversed.toList()) : primaryPathTmp; - final contacts = connector.allContacts; - final hops = _buildPathHops(primaryPath, contacts, l10n); + final hops = _buildPathHops(primaryPath, connector, l10n); final hasHopDetails = primaryPath.isNotEmpty; final observedLabel = _formatObservedHops( primaryPath.length, @@ -365,8 +364,7 @@ class _ChannelMessagePathMapScreenState : selectedPathTmp; final selectedIndex = _indexForPath(selectedPath, observedPaths); - final contacts = connector.allContacts; - final hops = _buildPathHops(selectedPath, contacts, context.l10n); + final hops = _buildPathHops(selectedPath, connector, context.l10n); final points = []; @@ -787,17 +785,62 @@ class _ObservedPath { List<_PathHop> _buildPathHops( Uint8List pathBytes, - List contacts, + MeshCoreConnector connector, AppLocalizations l10n, ) { + if (pathBytes.isEmpty) return const []; + final candidatesByPrefix = >{}; + for (final contact in connector.allContacts) { + if (contact.publicKey.isEmpty) continue; + if (contact.type != advTypeRepeater && contact.type != advTypeRoom) { + continue; + } + final prefix = contact.publicKey.first; + candidatesByPrefix.putIfAbsent(prefix, () => []).add(contact); + } + for (final candidates in candidatesByPrefix.values) { + candidates.sort((a, b) => b.lastSeen.compareTo(a.lastSeen)); + } + final startPoint = + (connector.selfLatitude != null && connector.selfLongitude != null) + ? LatLng(connector.selfLatitude!, connector.selfLongitude!) + : null; + var previousPosition = startPoint; + final distance = Distance(); + final hops = <_PathHop>[]; for (var i = 0; i < pathBytes.length; i++) { - final prefix = pathBytes[i]; - final contact = _matchContactForPrefix(contacts, prefix); + final searchPoint = i == 0 ? startPoint : previousPosition; + final candidates = candidatesByPrefix[pathBytes[i]]; + Contact? contact; + if (candidates != null && candidates.isNotEmpty) { + var bestIndex = 0; + if (searchPoint != null) { + var bestDistance = double.infinity; + for (var j = 0; j < candidates.length; j++) { + final candidate = candidates[j]; + if (!candidate.hasLocation) continue; + final currentDistance = distance( + searchPoint, + LatLng(candidate.latitude!, candidate.longitude!), + ); + if (currentDistance < bestDistance) { + bestDistance = currentDistance; + bestIndex = j; + } + } + } + contact = candidates.removeAt(bestIndex); + if (candidates.isEmpty) { + candidatesByPrefix.remove(pathBytes[i]); + } + } + + previousPosition = _resolvePosition(contact); hops.add( _PathHop( index: i + 1, - prefix: prefix, + prefix: pathBytes[i], contact: contact, position: _resolvePosition(contact), l10n: l10n, @@ -807,44 +850,12 @@ List<_PathHop> _buildPathHops( return hops; } -Contact? _matchContactForPrefix(List contacts, int prefix) { - final matches = contacts - .where( - (contact) => - (contact.type == advTypeRepeater || contact.type == advTypeRoom) && - contact.publicKey.isNotEmpty && - contact.publicKey[0] == prefix, - ) - .toList(); - if (matches.isEmpty) return null; - - Contact? pickWhere(bool Function(Contact) predicate) { - for (final contact in matches) { - if (predicate(contact)) return contact; - } - return null; - } - - return pickWhere((c) => c.type == advTypeRepeater && _hasValidLocation(c)) ?? - pickWhere((c) => c.type == advTypeRepeater) ?? - pickWhere(_hasValidLocation) ?? - matches.first; -} - LatLng? _resolvePosition(Contact? contact) { if (contact == null) return null; - if (!_hasValidLocation(contact)) return null; + if (!contact.hasLocation) return null; return LatLng(contact.latitude!, contact.longitude!); } -bool _hasValidLocation(Contact contact) { - final lat = contact.latitude; - final lon = contact.longitude; - if (lat == null || lon == null) return false; - if (lat == 0 && lon == 0) return false; - return true; -} - String _formatPrefix(int prefix) { return prefix.toRadixString(16).padLeft(2, '0').toUpperCase(); } From d2df2b0beda0f3826bd6d712be77e474e87b8066 Mon Sep 17 00:00:00 2001 From: ericszimmermann Date: Tue, 17 Mar 2026 22:23:23 +0100 Subject: [PATCH 2/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/screens/channel_message_path_screen.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index e2c5b49..d2f8d81 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -836,13 +836,16 @@ List<_PathHop> _buildPathHops( } } - previousPosition = _resolvePosition(contact); + final resolvedPosition = _resolvePosition(contact); + if (resolvedPosition != null) { + previousPosition = resolvedPosition; + } hops.add( _PathHop( index: i + 1, prefix: pathBytes[i], contact: contact, - position: _resolvePosition(contact), + position: resolvedPosition, l10n: l10n, ), ); From 11cb14a9256b53b43a937b55fe5810dbc2d10163 Mon Sep 17 00:00:00 2001 From: ericz Date: Tue, 17 Mar 2026 23:22:23 +0100 Subject: [PATCH 3/6] focus on hop if you click on one in the legend. --- lib/screens/channel_message_path_screen.dart | 29 +++++++++++++++++++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 ++ 2 files changed, 31 insertions(+) diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index d2f8d81..0092f22 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -302,10 +302,12 @@ class _ChannelMessagePathMapScreenState extends State { static const double _labelZoomThreshold = 8.5; + final MapController _mapController = MapController(); Uint8List? _selectedPath; double _pathDistance = 0.0; bool _showNodeLabels = true; bool _didReceivePositionUpdate = false; + int? _focusedHopIndex; @override void initState() { @@ -336,6 +338,22 @@ class _ChannelMessagePathMapScreenState return totalDistance; } + void _focusHop(_PathHop hop) { + if (!hop.hasLocation) return; + final targetZoom = _didReceivePositionUpdate + ? max(_mapController.camera.zoom, 14.0) + : 14.0; + _mapController.move(hop.position!, targetZoom); + } + + void _onHopTapped(_PathHop hop) { + _focusHop(hop); + if (!mounted) return; + setState(() { + _focusedHopIndex = hop.index; + }); + } + @override Widget build(BuildContext context) { return Consumer( @@ -419,6 +437,7 @@ class _ChannelMessagePathMapScreenState children: [ FlutterMap( key: mapKey, + mapController: _mapController, options: MapOptions( initialCenter: initialCenter, initialZoom: initialZoom, @@ -470,6 +489,7 @@ class _ChannelMessagePathMapScreenState ) { setState(() { _selectedPath = observedPaths[index].pathBytes; + _focusedHopIndex = null; }); }), if (points.isEmpty) @@ -725,8 +745,17 @@ class _ChannelMessagePathMapScreenState separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { final hop = hops[index]; + final isFocused = _focusedHopIndex == hop.index; return ListTile( dense: true, + enabled: hop.hasLocation, + selected: isFocused, + selectedTileColor: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.12), + onTap: hop.hasLocation + ? () => _onHopTapped(hop) + : null, leading: CircleAvatar( radius: 14, child: Text( diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d2ea57e..4084d9b 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner import package_info_plus +import path_provider_foundation import share_plus import shared_preferences_foundation import sqflite_darwin @@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) From 7b3c09973604c592bca8d3002edd537788333d2a Mon Sep 17 00:00:00 2001 From: ericz Date: Wed, 18 Mar 2026 06:52:08 +0100 Subject: [PATCH 4/6] reduce zoomlevel --- lib/screens/channel_message_path_screen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 0092f22..76273c4 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -341,8 +341,8 @@ class _ChannelMessagePathMapScreenState void _focusHop(_PathHop hop) { if (!hop.hasLocation) return; final targetZoom = _didReceivePositionUpdate - ? max(_mapController.camera.zoom, 14.0) - : 14.0; + ? max(_mapController.camera.zoom, 10.0) + : 12.0; _mapController.move(hop.position!, targetZoom); } From 87d11c2e6b3d0e1e21099ee9c39ba852c863e92d Mon Sep 17 00:00:00 2001 From: ericszimmermann Date: Wed, 18 Mar 2026 07:00:16 +0100 Subject: [PATCH 5/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/screens/channel_message_path_screen.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 76273c4..f64cb53 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -848,7 +848,11 @@ List<_PathHop> _buildPathHops( var bestDistance = double.infinity; for (var j = 0; j < candidates.length; j++) { final candidate = candidates[j]; - if (!candidate.hasLocation) continue; + if (!candidate.hasLocation || + candidate.latitude == null || + candidate.longitude == null) { + continue; + } final currentDistance = distance( searchPoint, LatLng(candidate.latitude!, candidate.longitude!), From b88e5e647ad9a0cda4a994210aa84ae6717148b0 Mon Sep 17 00:00:00 2001 From: ericszimmermann Date: Wed, 18 Mar 2026 08:06:22 +0100 Subject: [PATCH 6/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/screens/channel_message_path_screen.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index f64cb53..ea07eae 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -889,7 +889,10 @@ List<_PathHop> _buildPathHops( LatLng? _resolvePosition(Contact? contact) { if (contact == null) return null; if (!contact.hasLocation) return null; - return LatLng(contact.latitude!, contact.longitude!); + final latitude = contact.latitude; + final longitude = contact.longitude; + if (latitude == null || longitude == null) return null; + return LatLng(latitude, longitude); } String _formatPrefix(int prefix) {