Fix tracing path and display issues

This commit is contained in:
Janez T
2026-03-12 14:08:00 +01:00
parent b38a8c55d6
commit 2d1e6c0ca4
11 changed files with 1021 additions and 89 deletions

View File

@@ -13,6 +13,7 @@ import '../../providers/messages_provider.dart';
import '../../providers/sensors_provider.dart';
import '../../services/message_destination_preferences.dart';
import 'contact_route_dialog.dart';
import 'contact_trace_sheet.dart';
import 'room_login_sheet.dart';
import '../common/contact_avatar.dart';
import '../../utils/toast_logger.dart';
@@ -363,6 +364,15 @@ class ContactTile extends StatelessWidget {
_showSetRouteDialog(context, contact);
},
),
if (!contact.isChannel)
ListTile(
leading: const Icon(Icons.route),
title: const Text('Trace'),
onTap: () {
Navigator.pop(sheetContext);
_showTraceSheet(context, contact);
},
),
if (!contact.isPublicChannel)
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
@@ -453,6 +463,18 @@ class ContactTile extends StatelessWidget {
);
}
void _showTraceSheet(BuildContext context, Contact contact) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Theme.of(context).colorScheme.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => ContactTraceSheet(contact: contact),
);
}
void _showDeleteConfirmation(
BuildContext context,
Contact contact, {

View File

@@ -0,0 +1,525 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart' as flutter_map;
import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
import '../../models/contact.dart';
import '../../providers/connection_provider.dart';
import '../../providers/contacts_provider.dart';
import '../../services/mesh_map_nodes_service.dart';
import '../../utils/trace_node_resolver.dart';
class ContactTraceSheet extends StatefulWidget {
final Contact contact;
const ContactTraceSheet({super.key, required this.contact});
@override
State<ContactTraceSheet> createState() => _ContactTraceSheetState();
}
class _ContactTraceSheetState extends State<ContactTraceSheet> {
late final Future<_ContactTraceResult> _future;
@override
void initState() {
super.initState();
_future = _loadTrace();
}
Future<_ContactTraceResult> _loadTrace() async {
final connectionProvider = context.read<ConnectionProvider>();
final contactsProvider = context.read<ContactsProvider>();
final localNodes = _localNodesFromContacts(
contactsProvider,
connectionProvider: connectionProvider,
);
final localPublicKeys = localNodes.map((node) => node.publicKey).toSet();
var trace = _buildTraceResult(
nodes: localNodes,
localPublicKeys: localPublicKeys,
selfPublicKey: connectionProvider.deviceInfo.publicKey,
);
if (_isCompleteTrace(trace)) {
return trace;
}
unawaited(
MeshMapNodesService.syncInBackgroundIfStale(
cacheTtl: MeshMapNodesService.traceCacheTtl,
),
);
final remoteNodes = await MeshMapNodesService.loadCachedNodes(
cacheTtl: MeshMapNodesService.traceCacheTtl,
);
trace = _buildTraceResult(
nodes: _mergeNodes(localNodes, remoteNodes),
localPublicKeys: localPublicKeys,
selfPublicKey: connectionProvider.deviceInfo.publicKey,
);
return trace;
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: FutureBuilder<_ContactTraceResult>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const SizedBox(
height: 360,
child: Center(child: CircularProgressIndicator()),
);
}
if (snapshot.hasError) {
return SizedBox(
height: 360,
child: Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text('Failed to load trace: ${snapshot.error}'),
),
),
);
}
final trace = snapshot.data!;
final routeEntries = _displayRouteEntries(trace);
final concreteNodes = routeEntries
.where((entry) => entry.resolved.node != null)
.map((entry) => entry.resolved.node!)
.toList();
final mapPoints = concreteNodes
.map((node) => LatLng(node.latitude, node.longitude))
.toList();
final hasMapPath = mapPoints.length >= 2;
final relayNodes = trace.matchedRelayNodes.whereType<MeshMapNode>();
return SizedBox(
height: MediaQuery.of(context).size.height * 0.75,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Theme.of(context).dividerColor,
borderRadius: BorderRadius.circular(2),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 8),
child: Text(
'Trace',
style: Theme.of(context).textTheme.titleLarge,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
trace.routeHashes.isEmpty
? 'Direct route to ${widget.contact.displayName}'
: 'Route from saved contact path (${trace.routeHashes.length} hop${trace.routeHashes.length == 1 ? '' : 's'})',
style: Theme.of(context).textTheme.bodySmall,
),
),
const SizedBox(height: 10),
Expanded(
child: ListView(
padding: const EdgeInsets.only(bottom: 16),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox(
height: 240,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor,
),
),
child: hasMapPath
? flutter_map.FlutterMap(
options: flutter_map.MapOptions(
initialCameraFit:
flutter_map.CameraFit.bounds(
bounds:
flutter_map
.LatLngBounds.fromPoints(
mapPoints,
),
padding: const EdgeInsets.all(28),
),
),
children: [
flutter_map.TileLayer(
urlTemplate:
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName:
'com.meshcore.sar',
),
flutter_map.PolylineLayer(
polylines: [
flutter_map.Polyline(
points: mapPoints,
strokeWidth: 4,
color: Theme.of(
context,
).colorScheme.primary,
),
],
),
flutter_map.MarkerLayer(
markers: concreteNodes
.asMap()
.entries
.map(
(entry) => flutter_map.Marker(
point: LatLng(
entry.value.latitude,
entry.value.longitude,
),
width: 34,
height: 34,
child: CircleAvatar(
radius: 16,
backgroundColor:
entry.key == 0
? Colors.green
: (entry.key ==
concreteNodes
.length -
1
? Colors.red
: Colors.blue),
child: Text(
'${entry.key + 1}',
style: const TextStyle(
color: Colors.white,
fontWeight:
FontWeight.bold,
fontSize: 11,
),
),
),
),
)
.toList(),
),
],
)
: const Center(
child: Text(
'Not enough geolocated nodes to draw path',
),
),
),
),
),
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Route',
style: Theme.of(context).textTheme.titleMedium,
),
),
if (routeEntries.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Text(
'No named nodes could be matched for this trace.',
),
),
...routeEntries.asMap().entries.map(
(entry) => ListTile(
leading: CircleAvatar(
radius: 14,
backgroundColor: entry.key == 0
? Colors.green
: (entry.key == routeEntries.length - 1
? Colors.red
: Colors.blue),
child: Text(
'${entry.key + 1}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
),
title: Text(entry.value.label),
subtitle: Text(
'${_routeRoleLabel(entry.key, routeEntries.length)}${entry.value.keyLabel == null ? '' : '${entry.value.keyLabel}'}${entry.value.matchSummary == null ? '' : '${entry.value.matchSummary}'}',
),
),
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Relays (${relayNodes.length})',
style: Theme.of(context).textTheme.titleMedium,
),
),
if (relayNodes.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Text(
'No relay nodes could be matched for this contact.',
),
),
...relayNodes.map(
(node) => ListTile(
leading: const Icon(Icons.router),
title: Text(node.name),
subtitle: Text(
'${node.publicKey.substring(0, math.min(12, node.publicKey.length))}'
'${node.latitude.toStringAsFixed(5)}, ${node.longitude.toStringAsFixed(5)}',
),
),
),
],
),
),
],
),
);
},
),
);
}
List<_RouteDisplayEntry> _displayRouteEntries(_ContactTraceResult trace) {
final entries = <_RouteDisplayEntry>[];
if (trace.sender.node != null) {
entries.add(_RouteDisplayEntry.fromResolved(trace.sender));
}
entries.addAll(
trace.matchedRelayNodes.asMap().entries.map((entry) {
final resolved = entry.value;
final node = resolved.node;
final hashHex = trace.routeHashes[entry.key].toUpperCase();
return _RouteDisplayEntry(
resolved: resolved,
label: node?.name ?? 'Unknown',
keyLabel: node != null ? _prefixKeyLabel(node.publicKey) : hashHex,
matchSummary: resolved.matchSummary,
);
}),
);
if (trace.recipient.node != null) {
entries.add(_RouteDisplayEntry.fromResolved(trace.recipient));
}
return entries;
}
String _routeRoleLabel(int index, int total) {
if (index == 0) return 'Sender';
if (index == total - 1) return 'Recipient';
return 'Relay';
}
String _prefixKeyLabel(String publicKey) =>
publicKey.substring(0, math.min(12, publicKey.length));
bool _isCompleteTrace(_ContactTraceResult trace) {
if (trace.sender.node == null || trace.recipient.node == null) {
return false;
}
if (trace.routeHashes.isEmpty) {
return true;
}
return trace.matchedRelayNodes.every((node) => node.node != null);
}
_ContactTraceResult _buildTraceResult({
required List<MeshMapNode> nodes,
required Set<String> localPublicKeys,
required List<int>? selfPublicKey,
}) {
final senderNode = TraceNodeResolver.resolveBest(
nodes: nodes,
localPublicKeys: localPublicKeys,
prefixHex: _toPrefixHex(selfPublicKey),
);
final recipientNode = TraceNodeResolver.resolveBest(
nodes: nodes,
localPublicKeys: localPublicKeys,
prefixHex: _toPrefixHex(widget.contact.publicKey),
);
final senderLatLng = senderNode.node == null
? null
: LatLng(senderNode.node!.latitude, senderNode.node!.longitude);
final recipientLatLng = recipientNode.node == null
? null
: LatLng(recipientNode.node!.latitude, recipientNode.node!.longitude);
final routeHashes =
widget.contact.routeHasPath && widget.contact.routeHopCount > 0
? widget.contact.routeCanonicalText
.split(',')
.where((token) => token.isNotEmpty)
.map((token) => token.toLowerCase())
.toList()
: const <String>[];
final matchedRelayNodes = routeHashes
.map(
(hash) => TraceNodeResolver.resolveBest(
nodes: nodes,
localPublicKeys: localPublicKeys,
prefixHex: hash,
referenceA: senderLatLng,
referenceB: recipientLatLng,
),
)
.toList();
return _ContactTraceResult(
sender: senderNode,
recipient: recipientNode,
routeHashes: routeHashes,
matchedRelayNodes: matchedRelayNodes,
);
}
String? _toPrefixHex(List<int>? key) {
if (key == null || key.isEmpty) return null;
final take = key.length < 6 ? key.length : 6;
return key
.take(take)
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join()
.toLowerCase();
}
List<MeshMapNode> _localNodesFromContacts(
ContactsProvider contactsProvider, {
required ConnectionProvider connectionProvider,
}) {
final nodes = contactsProvider.contactsWithLocation
.map((contact) {
final location = contact.displayLocation;
if (location == null) return null;
return MeshMapNode(
type: contact.type.index,
name: contact.displayName,
publicKey: contact.publicKeyHex.toLowerCase(),
latitude: location.latitude,
longitude: location.longitude,
updatedAtMs: contact.lastAdvert * 1000,
);
})
.whereType<MeshMapNode>()
.toList();
final selfNode = _selfNode(connectionProvider);
if (selfNode != null) {
nodes.add(selfNode);
}
return nodes;
}
MeshMapNode? _selfNode(ConnectionProvider connectionProvider) {
final publicKey = connectionProvider.deviceInfo.publicKey;
final advLat = connectionProvider.deviceInfo.advLat;
final advLon = connectionProvider.deviceInfo.advLon;
if (publicKey == null || advLat == null || advLon == null) {
return null;
}
if (advLat == 0 && advLon == 0) {
return null;
}
return MeshMapNode(
type: -1,
name: connectionProvider.deviceInfo.selfName?.trim().isNotEmpty == true
? connectionProvider.deviceInfo.selfName!.trim()
: 'You',
publicKey: publicKey
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join()
.toLowerCase(),
latitude: advLat / 1e6,
longitude: advLon / 1e6,
updatedAtMs: DateTime.now().millisecondsSinceEpoch,
);
}
List<MeshMapNode> _mergeNodes(
List<MeshMapNode> preferred,
List<MeshMapNode> fallback,
) {
final merged = <String, MeshMapNode>{};
for (final node in fallback) {
merged[node.publicKey] = node;
}
for (final node in preferred) {
merged[node.publicKey] = node;
}
return merged.values.toList();
}
}
class _ContactTraceResult {
final ResolvedTraceNode sender;
final ResolvedTraceNode recipient;
final List<String> routeHashes;
final List<ResolvedTraceNode> matchedRelayNodes;
const _ContactTraceResult({
required this.sender,
required this.recipient,
required this.routeHashes,
required this.matchedRelayNodes,
});
}
class _RouteDisplayEntry {
final ResolvedTraceNode resolved;
final String label;
final String? keyLabel;
final String? matchSummary;
const _RouteDisplayEntry({
required this.resolved,
required this.label,
required this.keyLabel,
required this.matchSummary,
});
MeshMapNode? get node => resolved.node;
factory _RouteDisplayEntry.fromResolved(ResolvedTraceNode resolved) {
final node = resolved.node!;
return _RouteDisplayEntry(
resolved: resolved,
label: node.name,
keyLabel: node.publicKey.substring(
0,
math.min(12, node.publicKey.length),
),
matchSummary: resolved.matchSummary,
);
}
}

View File

@@ -14,6 +14,7 @@ import '../../providers/messages_provider.dart';
import '../../services/mesh_map_nodes_service.dart';
import '../../services/route_hash_preferences.dart';
import '../../utils/log_rx_route_decoder.dart';
import '../../utils/trace_node_resolver.dart';
class MessageTraceSheet extends StatefulWidget {
final Message message;
@@ -38,10 +39,10 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
final contactsProvider = context.read<ContactsProvider>();
final messagesProvider = context.read<MessagesProvider>();
final preferredHashSize = await RouteHashPreferences.getHashSize();
final storedPath =
messagesProvider.getMessageReceptionDetails(widget.message.id)?.pathBytes;
final packetPath =
(storedPath != null && storedPath.isNotEmpty)
final storedPath = messagesProvider
.getMessageReceptionDetails(widget.message.id)
?.pathBytes;
final packetPath = (storedPath != null && storedPath.isNotEmpty)
? storedPath
: _extractPathFromPacketLogs(
logs: connectionProvider.bleService.packetLogs,
@@ -54,8 +55,10 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
: _toPrefixHex(connectionProvider.deviceInfo.publicKey);
final localNodes = _localNodesFromContacts(contactsProvider);
final localPublicKeys = localNodes.map((node) => node.publicKey).toSet();
var trace = _buildTraceResult(
nodes: localNodes,
localPublicKeys: localPublicKeys,
packetPath: packetPath,
preferredHashSize: preferredHashSize,
senderPrefix: senderPrefix,
@@ -79,6 +82,7 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
);
trace = _buildTraceResult(
nodes: _mergeNodes(localNodes, remoteNodes),
localPublicKeys: localPublicKeys,
packetPath: packetPath,
preferredHashSize: preferredHashSize,
senderPrefix: senderPrefix,
@@ -114,8 +118,8 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
final trace = snapshot.data!;
final routeEntries = _displayRouteEntries(trace);
final concretePathNodes = routeEntries
.where((entry) => entry.node != null)
.map((entry) => entry.node!)
.where((entry) => entry.resolved.node != null)
.map((entry) => entry.resolved.node!)
.toList();
final mapPoints = concretePathNodes
.map((n) => LatLng(n.latitude, n.longitude))
@@ -289,7 +293,7 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
),
title: Text(entry.value.label),
subtitle: Text(
'${_routeRoleLabel(entry.key, routeEntries.length)}${entry.value.keyLabel == null ? '' : '${entry.value.keyLabel}'}',
'${_routeRoleLabel(entry.key, routeEntries.length)}${entry.value.keyLabel == null ? '' : '${entry.value.keyLabel}'}${entry.value.matchSummary == null ? '' : '${entry.value.matchSummary}'}',
),
),
),
@@ -333,7 +337,10 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
}
List<MeshMapNode> _relayNodes(_TraceResult trace) {
final concrete = trace.matchedPathNodes.whereType<MeshMapNode>().toList();
final concrete = trace.matchedPathNodes
.map((entry) => entry.node)
.whereType<MeshMapNode>()
.toList();
if (concrete.isEmpty) return const [];
if (trace.mode == TraceMode.packetPath) {
@@ -346,13 +353,17 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
}
List<_RouteDisplayEntry> _displayRouteEntries(_TraceResult trace) {
final pathNodes = trace.matchedPathNodes.whereType<MeshMapNode>().toList();
final pathNodes = trace.matchedPathNodes
.map((entry) => entry.node)
.whereType<MeshMapNode>()
.toList();
if (pathNodes.isEmpty) {
return [
if (trace.sender != null) _RouteDisplayEntry.fromNode(trace.sender!),
if (trace.recipient != null &&
trace.recipient!.publicKey != trace.sender?.publicKey)
_RouteDisplayEntry.fromNode(trace.recipient!),
if (trace.sender.node != null)
_RouteDisplayEntry.fromResolved(trace.sender),
if (trace.recipient.node != null &&
trace.recipient.node!.publicKey != trace.sender.node?.publicKey)
_RouteDisplayEntry.fromResolved(trace.recipient),
];
}
@@ -360,29 +371,34 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
final entries = trace.matchedPathNodes.asMap().entries.map((entry) {
final hashHex = trace.pathHashes[entry.key].toUpperCase();
return _RouteDisplayEntry(
node: entry.value,
label: entry.value?.name ?? 'Unknown',
keyLabel: entry.value != null
? _prefixKeyLabel(entry.value!.publicKey)
resolved: entry.value,
label: entry.value.node?.name ?? 'Unknown',
keyLabel: entry.value.node != null
? _prefixKeyLabel(entry.value.node!.publicKey)
: hashHex,
matchSummary: entry.value.matchSummary,
);
}).toList();
final lastKey = pathNodes.last.publicKey;
return [
...entries,
if (trace.recipient != null && trace.recipient!.publicKey != lastKey)
_RouteDisplayEntry.fromNode(trace.recipient!),
if (trace.recipient.node != null &&
trace.recipient.node!.publicKey != lastKey)
_RouteDisplayEntry.fromResolved(trace.recipient),
];
}
final firstKey = pathNodes.first.publicKey;
final lastKey = pathNodes.last.publicKey;
return [
if (trace.sender != null && trace.sender!.publicKey != firstKey)
_RouteDisplayEntry.fromNode(trace.sender!),
...pathNodes.map(_RouteDisplayEntry.fromNode),
if (trace.recipient != null && trace.recipient!.publicKey != lastKey)
_RouteDisplayEntry.fromNode(trace.recipient!),
if (trace.sender.node != null && trace.sender.node!.publicKey != firstKey)
_RouteDisplayEntry.fromResolved(trace.sender),
...trace.matchedPathNodes
.where((entry) => entry.node != null)
.map(_RouteDisplayEntry.fromResolved),
if (trace.recipient.node != null &&
trace.recipient.node!.publicKey != lastKey)
_RouteDisplayEntry.fromResolved(trace.recipient),
];
}
@@ -405,14 +421,6 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
.toLowerCase();
}
MeshMapNode? _bestNodeForPrefix(List<MeshMapNode> nodes, String? prefixHex) {
if (prefixHex == null || prefixHex.isEmpty) return null;
final matches =
nodes.where((n) => n.publicKey.startsWith(prefixHex)).toList()
..sort((a, b) => b.updatedAtMs.compareTo(a.updatedAtMs));
return matches.isEmpty ? null : matches.first;
}
List<MeshMapNode> _localNodesFromContacts(ContactsProvider contactsProvider) {
return contactsProvider.contactsWithLocation
.map((contact) {
@@ -447,13 +455,28 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
_TraceResult _buildTraceResult({
required List<MeshMapNode> nodes,
required Set<String> localPublicKeys,
required List<int>? packetPath,
required int preferredHashSize,
required String? senderPrefix,
required String? recipientPrefix,
}) {
final senderNode = _bestNodeForPrefix(nodes, senderPrefix);
final recipientNode = _bestNodeForPrefix(nodes, recipientPrefix);
final senderNode = TraceNodeResolver.resolveBest(
nodes: nodes,
localPublicKeys: localPublicKeys,
prefixHex: senderPrefix,
);
final recipientNode = TraceNodeResolver.resolveBest(
nodes: nodes,
localPublicKeys: localPublicKeys,
prefixHex: recipientPrefix,
);
final senderLatLng = senderNode.node == null
? null
: LatLng(senderNode.node!.latitude, senderNode.node!.longitude);
final recipientLatLng = recipientNode.node == null
? null
: LatLng(recipientNode.node!.latitude, recipientNode.node!.longitude);
if (packetPath != null && packetPath.isNotEmpty) {
final hashSize = LogRxRouteDecoder.inferHashSize(
@@ -466,9 +489,12 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
).reversed.toList();
final matched = _matchNodesFromPathHashes(
nodes: nodes,
localPublicKeys: localPublicKeys,
pathHashes: hopHashes,
senderPrefix: senderPrefix,
recipientPrefix: recipientPrefix,
senderLatLng: senderLatLng,
recipientLatLng: recipientLatLng,
);
return _TraceResult(
mode: TraceMode.packetPath,
@@ -481,14 +507,20 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
final inferred = _inferRelaysFromHopCount(
nodes: nodes,
sender: senderNode,
recipient: recipientNode,
sender: senderNode.node,
recipient: recipientNode.node,
relayCount: math.max(0, widget.message.pathLen),
);
final matchedPathNodes = <MeshMapNode?>[
if (senderNode != null) senderNode,
...inferred,
if (recipientNode != null) recipientNode,
final matchedPathNodes = <ResolvedTraceNode>[
if (senderNode.node != null) senderNode,
...inferred.map(
(node) => ResolvedTraceNode(
node: node,
matchCount: 1,
usedOnlineFallback: false,
),
),
if (recipientNode.node != null) recipientNode,
];
return _TraceResult(
@@ -501,16 +533,17 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
}
bool _isCompleteTrace(_TraceResult trace, {required int expectedRelayCount}) {
if (trace.sender == null || trace.recipient == null) {
if (trace.sender.node == null || trace.recipient.node == null) {
return false;
}
if (trace.mode == TraceMode.packetPath) {
return trace.matchedPathNodes.length == trace.pathHashes.length &&
trace.matchedPathNodes.every((node) => node != null);
trace.matchedPathNodes.every((node) => node.node != null);
}
final concreteCount = trace.matchedPathNodes
.map((entry) => entry.node)
.whereType<MeshMapNode>()
.length;
return concreteCount >= expectedRelayCount + 2;
@@ -551,39 +584,30 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
return decoded.pathBytes;
}
List<MeshMapNode?> _matchNodesFromPathHashes({
List<ResolvedTraceNode> _matchNodesFromPathHashes({
required List<MeshMapNode> nodes,
required Set<String> localPublicKeys,
required List<String> pathHashes,
required String? senderPrefix,
required String? recipientPrefix,
required LatLng? senderLatLng,
required LatLng? recipientLatLng,
}) {
final result = <MeshMapNode?>[];
final result = <ResolvedTraceNode>[];
for (var i = 0; i < pathHashes.length; i++) {
final hashHex = pathHashes[i].toLowerCase();
final candidates = nodes
.where((n) => n.publicKey.startsWith(hashHex))
.toList();
if (candidates.isEmpty) {
result.add(null);
continue;
}
List<MeshMapNode> filtered = candidates;
if (i == 0 && senderPrefix != null) {
final senderMatches = filtered
.where((n) => n.publicKey.startsWith(senderPrefix))
.toList();
if (senderMatches.isNotEmpty) filtered = senderMatches;
}
if (i == pathHashes.length - 1 && recipientPrefix != null) {
final recipientMatches = filtered
.where((n) => n.publicKey.startsWith(recipientPrefix))
.toList();
if (recipientMatches.isNotEmpty) filtered = recipientMatches;
}
filtered.sort((a, b) => b.updatedAtMs.compareTo(a.updatedAtMs));
result.add(filtered.first);
result.add(
TraceNodeResolver.resolveBest(
nodes: nodes,
localPublicKeys: localPublicKeys,
prefixHex: hashHex,
preferredPrefix: i == 0
? senderPrefix
: (i == pathHashes.length - 1 ? recipientPrefix : null),
referenceA: senderLatLng,
referenceB: recipientLatLng,
),
);
}
return result;
}
@@ -652,10 +676,10 @@ enum TraceMode { packetPath, hopCountInference }
class _TraceResult {
final TraceMode mode;
final MeshMapNode? sender;
final MeshMapNode? recipient;
final ResolvedTraceNode sender;
final ResolvedTraceNode recipient;
final List<String> pathHashes;
final List<MeshMapNode?> matchedPathNodes;
final List<ResolvedTraceNode> matchedPathNodes;
const _TraceResult({
required this.mode,
@@ -667,24 +691,30 @@ class _TraceResult {
}
class _RouteDisplayEntry {
final MeshMapNode? node;
final ResolvedTraceNode resolved;
final String label;
final String? keyLabel;
final String? matchSummary;
const _RouteDisplayEntry({
required this.node,
required this.resolved,
required this.label,
required this.keyLabel,
required this.matchSummary,
});
factory _RouteDisplayEntry.fromNode(MeshMapNode node) {
MeshMapNode? get node => resolved.node;
factory _RouteDisplayEntry.fromResolved(ResolvedTraceNode resolved) {
final node = resolved.node!;
return _RouteDisplayEntry(
node: node,
resolved: resolved,
label: node.name,
keyLabel: node.publicKey.substring(
0,
math.min(12, node.publicKey.length),
),
matchSummary: resolved.matchSummary,
);
}
}