mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Warn channels about delayed message
This commit is contained in:
@@ -3,24 +3,42 @@ import 'package:latlong2/latlong.dart';
|
|||||||
import '../services/mesh_map_nodes_service.dart';
|
import '../services/mesh_map_nodes_service.dart';
|
||||||
|
|
||||||
class ResolvedTraceNode {
|
class ResolvedTraceNode {
|
||||||
final MeshMapNode? node;
|
final List<MeshMapNode> candidates;
|
||||||
final int matchCount;
|
final int matchCount;
|
||||||
final bool usedOnlineFallback;
|
final bool usedOnlineFallback;
|
||||||
|
final int selectedIndex;
|
||||||
|
|
||||||
const ResolvedTraceNode({
|
const ResolvedTraceNode({
|
||||||
required this.node,
|
required this.candidates,
|
||||||
required this.matchCount,
|
required this.matchCount,
|
||||||
required this.usedOnlineFallback,
|
required this.usedOnlineFallback,
|
||||||
|
this.selectedIndex = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
MeshMapNode? get node =>
|
||||||
|
candidates.isEmpty ? null : candidates[selectedIndex];
|
||||||
bool get hasMatch => node != null;
|
bool get hasMatch => node != null;
|
||||||
bool get isAmbiguous => matchCount > 1;
|
bool get isAmbiguous => matchCount > 1;
|
||||||
|
bool get canCycle => candidates.length > 1;
|
||||||
|
|
||||||
String? get matchSummary {
|
String? get matchSummary {
|
||||||
if (matchCount <= 1) return null;
|
if (matchCount <= 1) return null;
|
||||||
final source = usedOnlineFallback ? 'online' : 'local';
|
final source = usedOnlineFallback ? 'online' : 'local';
|
||||||
return '$matchCount $source matches';
|
return '$matchCount $source matches';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? get cycleSummary =>
|
||||||
|
canCycle ? 'tap to cycle ${selectedIndex + 1}/$matchCount' : null;
|
||||||
|
|
||||||
|
ResolvedTraceNode cycle() {
|
||||||
|
if (!canCycle) return this;
|
||||||
|
return ResolvedTraceNode(
|
||||||
|
candidates: candidates,
|
||||||
|
matchCount: matchCount,
|
||||||
|
usedOnlineFallback: usedOnlineFallback,
|
||||||
|
selectedIndex: (selectedIndex + 1) % candidates.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TraceNodeResolver {
|
class TraceNodeResolver {
|
||||||
@@ -38,7 +56,7 @@ class TraceNodeResolver {
|
|||||||
}) {
|
}) {
|
||||||
if (prefixHex == null || prefixHex.isEmpty) {
|
if (prefixHex == null || prefixHex.isEmpty) {
|
||||||
return const ResolvedTraceNode(
|
return const ResolvedTraceNode(
|
||||||
node: null,
|
candidates: <MeshMapNode>[],
|
||||||
matchCount: 0,
|
matchCount: 0,
|
||||||
usedOnlineFallback: false,
|
usedOnlineFallback: false,
|
||||||
);
|
);
|
||||||
@@ -49,7 +67,7 @@ class TraceNodeResolver {
|
|||||||
.toList();
|
.toList();
|
||||||
if (allMatches.isEmpty) {
|
if (allMatches.isEmpty) {
|
||||||
return const ResolvedTraceNode(
|
return const ResolvedTraceNode(
|
||||||
node: null,
|
candidates: <MeshMapNode>[],
|
||||||
matchCount: 0,
|
matchCount: 0,
|
||||||
usedOnlineFallback: false,
|
usedOnlineFallback: false,
|
||||||
);
|
);
|
||||||
@@ -84,7 +102,7 @@ class TraceNodeResolver {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return ResolvedTraceNode(
|
return ResolvedTraceNode(
|
||||||
node: pool.first,
|
candidates: List<MeshMapNode>.unmodifiable(pool),
|
||||||
matchCount: pool.length,
|
matchCount: pool.length,
|
||||||
usedOnlineFallback: usedOnlineFallback,
|
usedOnlineFallback: usedOnlineFallback,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class ContactTraceSheet extends StatefulWidget {
|
|||||||
|
|
||||||
class _ContactTraceSheetState extends State<ContactTraceSheet> {
|
class _ContactTraceSheetState extends State<ContactTraceSheet> {
|
||||||
late final Future<_ContactTraceResult> _future;
|
late final Future<_ContactTraceResult> _future;
|
||||||
|
_ContactTraceResult? _traceOverride;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -89,7 +90,7 @@ class _ContactTraceSheetState extends State<ContactTraceSheet> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final trace = snapshot.data!;
|
final trace = _traceOverride ?? snapshot.data!;
|
||||||
final routeEntries = _displayRouteEntries(trace);
|
final routeEntries = _displayRouteEntries(trace);
|
||||||
final concreteNodes = routeEntries
|
final concreteNodes = routeEntries
|
||||||
.where((entry) => entry.resolved.node != null)
|
.where((entry) => entry.resolved.node != null)
|
||||||
@@ -99,7 +100,9 @@ class _ContactTraceSheetState extends State<ContactTraceSheet> {
|
|||||||
.map((node) => LatLng(node.latitude, node.longitude))
|
.map((node) => LatLng(node.latitude, node.longitude))
|
||||||
.toList();
|
.toList();
|
||||||
final hasMapPath = mapPoints.length >= 2;
|
final hasMapPath = mapPoints.length >= 2;
|
||||||
final relayNodes = trace.matchedRelayNodes.whereType<MeshMapNode>();
|
final relayNodes = trace.matchedRelayNodes
|
||||||
|
.map((entry) => entry.node)
|
||||||
|
.whereType<MeshMapNode>();
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: MediaQuery.of(context).size.height * 0.75,
|
height: MediaQuery.of(context).size.height * 0.75,
|
||||||
@@ -249,6 +252,15 @@ class _ContactTraceSheetState extends State<ContactTraceSheet> {
|
|||||||
),
|
),
|
||||||
...routeEntries.asMap().entries.map(
|
...routeEntries.asMap().entries.map(
|
||||||
(entry) => ListTile(
|
(entry) => ListTile(
|
||||||
|
onTap: entry.value.resolved.canCycle
|
||||||
|
? () => setState(() {
|
||||||
|
final baseTrace =
|
||||||
|
_traceOverride ?? snapshot.data!;
|
||||||
|
_traceOverride = baseTrace.cycleEntry(
|
||||||
|
entry.value.target,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null,
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
radius: 14,
|
radius: 14,
|
||||||
backgroundColor: entry.key == 0
|
backgroundColor: entry.key == 0
|
||||||
@@ -267,8 +279,11 @@ class _ContactTraceSheetState extends State<ContactTraceSheet> {
|
|||||||
),
|
),
|
||||||
title: Text(entry.value.label),
|
title: Text(entry.value.label),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
'${_routeRoleLabel(entry.key, routeEntries.length)}${entry.value.keyLabel == null ? '' : ' • ${entry.value.keyLabel}'}${entry.value.matchSummary == null ? '' : ' • ${entry.value.matchSummary}'}',
|
'${_routeRoleLabel(entry.key, routeEntries.length)}${entry.value.keyLabel == null ? '' : ' • ${entry.value.keyLabel}'}${entry.value.matchSummary == null ? '' : ' • ${entry.value.matchSummary}'}${entry.value.resolved.cycleSummary == null ? '' : ' • ${entry.value.resolved.cycleSummary}'}',
|
||||||
),
|
),
|
||||||
|
trailing: entry.value.resolved.canCycle
|
||||||
|
? const Icon(Icons.sync_alt)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -313,7 +328,12 @@ class _ContactTraceSheetState extends State<ContactTraceSheet> {
|
|||||||
List<_RouteDisplayEntry> _displayRouteEntries(_ContactTraceResult trace) {
|
List<_RouteDisplayEntry> _displayRouteEntries(_ContactTraceResult trace) {
|
||||||
final entries = <_RouteDisplayEntry>[];
|
final entries = <_RouteDisplayEntry>[];
|
||||||
if (trace.sender.node != null) {
|
if (trace.sender.node != null) {
|
||||||
entries.add(_RouteDisplayEntry.fromResolved(trace.sender));
|
entries.add(
|
||||||
|
_RouteDisplayEntry.fromResolved(
|
||||||
|
trace.sender,
|
||||||
|
target: const _RouteEntryTarget.sender(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
entries.addAll(
|
entries.addAll(
|
||||||
trace.matchedRelayNodes.asMap().entries.map((entry) {
|
trace.matchedRelayNodes.asMap().entries.map((entry) {
|
||||||
@@ -325,11 +345,17 @@ class _ContactTraceSheetState extends State<ContactTraceSheet> {
|
|||||||
label: node?.name ?? 'Unknown',
|
label: node?.name ?? 'Unknown',
|
||||||
keyLabel: node != null ? _prefixKeyLabel(node.publicKey) : hashHex,
|
keyLabel: node != null ? _prefixKeyLabel(node.publicKey) : hashHex,
|
||||||
matchSummary: resolved.matchSummary,
|
matchSummary: resolved.matchSummary,
|
||||||
|
target: _RouteEntryTarget.relayNode(entry.key),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (trace.recipient.node != null) {
|
if (trace.recipient.node != null) {
|
||||||
entries.add(_RouteDisplayEntry.fromResolved(trace.recipient));
|
entries.add(
|
||||||
|
_RouteDisplayEntry.fromResolved(
|
||||||
|
trace.recipient,
|
||||||
|
target: const _RouteEntryTarget.recipient(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
@@ -493,6 +519,34 @@ class _ContactTraceResult {
|
|||||||
required this.routeHashes,
|
required this.routeHashes,
|
||||||
required this.matchedRelayNodes,
|
required this.matchedRelayNodes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_ContactTraceResult cycleEntry(_RouteEntryTarget target) {
|
||||||
|
switch (target.kind) {
|
||||||
|
case _RouteEntryKind.sender:
|
||||||
|
return _ContactTraceResult(
|
||||||
|
sender: sender.cycle(),
|
||||||
|
recipient: recipient,
|
||||||
|
routeHashes: routeHashes,
|
||||||
|
matchedRelayNodes: matchedRelayNodes,
|
||||||
|
);
|
||||||
|
case _RouteEntryKind.recipient:
|
||||||
|
return _ContactTraceResult(
|
||||||
|
sender: sender,
|
||||||
|
recipient: recipient.cycle(),
|
||||||
|
routeHashes: routeHashes,
|
||||||
|
matchedRelayNodes: matchedRelayNodes,
|
||||||
|
);
|
||||||
|
case _RouteEntryKind.relayNode:
|
||||||
|
final updated = matchedRelayNodes.toList();
|
||||||
|
updated[target.index] = updated[target.index].cycle();
|
||||||
|
return _ContactTraceResult(
|
||||||
|
sender: sender,
|
||||||
|
recipient: recipient,
|
||||||
|
routeHashes: routeHashes,
|
||||||
|
matchedRelayNodes: updated,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RouteDisplayEntry {
|
class _RouteDisplayEntry {
|
||||||
@@ -500,17 +554,22 @@ class _RouteDisplayEntry {
|
|||||||
final String label;
|
final String label;
|
||||||
final String? keyLabel;
|
final String? keyLabel;
|
||||||
final String? matchSummary;
|
final String? matchSummary;
|
||||||
|
final _RouteEntryTarget target;
|
||||||
|
|
||||||
const _RouteDisplayEntry({
|
const _RouteDisplayEntry({
|
||||||
required this.resolved,
|
required this.resolved,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.keyLabel,
|
required this.keyLabel,
|
||||||
required this.matchSummary,
|
required this.matchSummary,
|
||||||
|
required this.target,
|
||||||
});
|
});
|
||||||
|
|
||||||
MeshMapNode? get node => resolved.node;
|
MeshMapNode? get node => resolved.node;
|
||||||
|
|
||||||
factory _RouteDisplayEntry.fromResolved(ResolvedTraceNode resolved) {
|
factory _RouteDisplayEntry.fromResolved(
|
||||||
|
ResolvedTraceNode resolved, {
|
||||||
|
required _RouteEntryTarget target,
|
||||||
|
}) {
|
||||||
final node = resolved.node!;
|
final node = resolved.node!;
|
||||||
return _RouteDisplayEntry(
|
return _RouteDisplayEntry(
|
||||||
resolved: resolved,
|
resolved: resolved,
|
||||||
@@ -520,6 +579,21 @@ class _RouteDisplayEntry {
|
|||||||
math.min(12, node.publicKey.length),
|
math.min(12, node.publicKey.length),
|
||||||
),
|
),
|
||||||
matchSummary: resolved.matchSummary,
|
matchSummary: resolved.matchSummary,
|
||||||
|
target: target,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum _RouteEntryKind { sender, recipient, relayNode }
|
||||||
|
|
||||||
|
class _RouteEntryTarget {
|
||||||
|
final _RouteEntryKind kind;
|
||||||
|
final int index;
|
||||||
|
|
||||||
|
const _RouteEntryTarget._(this.kind, [this.index = 0]);
|
||||||
|
|
||||||
|
const _RouteEntryTarget.sender() : this._(_RouteEntryKind.sender);
|
||||||
|
const _RouteEntryTarget.recipient() : this._(_RouteEntryKind.recipient);
|
||||||
|
const _RouteEntryTarget.relayNode(int index)
|
||||||
|
: this._(_RouteEntryKind.relayNode, index);
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class MessageTraceSheet extends StatefulWidget {
|
|||||||
|
|
||||||
class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
||||||
late final Future<_TraceResult> _future;
|
late final Future<_TraceResult> _future;
|
||||||
|
_TraceResult? _traceOverride;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -53,7 +54,6 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
|||||||
final recipientPrefix = widget.message.recipientPublicKey != null
|
final recipientPrefix = widget.message.recipientPublicKey != null
|
||||||
? _toPrefixHex(widget.message.recipientPublicKey)
|
? _toPrefixHex(widget.message.recipientPublicKey)
|
||||||
: _toPrefixHex(connectionProvider.deviceInfo.publicKey);
|
: _toPrefixHex(connectionProvider.deviceInfo.publicKey);
|
||||||
|
|
||||||
final localNodes = _localNodesFromContacts(contactsProvider);
|
final localNodes = _localNodesFromContacts(contactsProvider);
|
||||||
final localPublicKeys = localNodes.map((node) => node.publicKey).toSet();
|
final localPublicKeys = localNodes.map((node) => node.publicKey).toSet();
|
||||||
var trace = _buildTraceResult(
|
var trace = _buildTraceResult(
|
||||||
@@ -115,7 +115,7 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final trace = snapshot.data!;
|
final trace = _traceOverride ?? snapshot.data!;
|
||||||
final routeEntries = _displayRouteEntries(trace);
|
final routeEntries = _displayRouteEntries(trace);
|
||||||
final concretePathNodes = routeEntries
|
final concretePathNodes = routeEntries
|
||||||
.where((entry) => entry.resolved.node != null)
|
.where((entry) => entry.resolved.node != null)
|
||||||
@@ -275,6 +275,15 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
|||||||
),
|
),
|
||||||
...routeEntries.asMap().entries.map(
|
...routeEntries.asMap().entries.map(
|
||||||
(entry) => ListTile(
|
(entry) => ListTile(
|
||||||
|
onTap: entry.value.resolved.canCycle
|
||||||
|
? () => setState(() {
|
||||||
|
final baseTrace =
|
||||||
|
_traceOverride ?? snapshot.data!;
|
||||||
|
_traceOverride = baseTrace.cycleEntry(
|
||||||
|
entry.value.target,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null,
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
radius: 14,
|
radius: 14,
|
||||||
backgroundColor: entry.key == 0
|
backgroundColor: entry.key == 0
|
||||||
@@ -293,8 +302,11 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
|||||||
),
|
),
|
||||||
title: Text(entry.value.label),
|
title: Text(entry.value.label),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
'${_routeRoleLabel(entry.key, routeEntries.length)}${entry.value.keyLabel == null ? '' : ' • ${entry.value.keyLabel}'}${entry.value.matchSummary == null ? '' : ' • ${entry.value.matchSummary}'}',
|
'${_routeRoleLabel(entry.key, routeEntries.length)}${entry.value.keyLabel == null ? '' : ' • ${entry.value.keyLabel}'}${entry.value.matchSummary == null ? '' : ' • ${entry.value.matchSummary}'}${entry.value.resolved.cycleSummary == null ? '' : ' • ${entry.value.resolved.cycleSummary}'}',
|
||||||
),
|
),
|
||||||
|
trailing: entry.value.resolved.canCycle
|
||||||
|
? const Icon(Icons.sync_alt)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -360,10 +372,16 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
|||||||
if (pathNodes.isEmpty) {
|
if (pathNodes.isEmpty) {
|
||||||
return [
|
return [
|
||||||
if (trace.sender.node != null)
|
if (trace.sender.node != null)
|
||||||
_RouteDisplayEntry.fromResolved(trace.sender),
|
_RouteDisplayEntry.fromResolved(
|
||||||
|
trace.sender,
|
||||||
|
target: const _RouteEntryTarget.sender(),
|
||||||
|
),
|
||||||
if (trace.recipient.node != null &&
|
if (trace.recipient.node != null &&
|
||||||
trace.recipient.node!.publicKey != trace.sender.node?.publicKey)
|
trace.recipient.node!.publicKey != trace.sender.node?.publicKey)
|
||||||
_RouteDisplayEntry.fromResolved(trace.recipient),
|
_RouteDisplayEntry.fromResolved(
|
||||||
|
trace.recipient,
|
||||||
|
target: const _RouteEntryTarget.recipient(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,6 +395,7 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
|||||||
? _prefixKeyLabel(entry.value.node!.publicKey)
|
? _prefixKeyLabel(entry.value.node!.publicKey)
|
||||||
: hashHex,
|
: hashHex,
|
||||||
matchSummary: entry.value.matchSummary,
|
matchSummary: entry.value.matchSummary,
|
||||||
|
target: _RouteEntryTarget.pathNode(entry.key),
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
final lastKey = pathNodes.last.publicKey;
|
final lastKey = pathNodes.last.publicKey;
|
||||||
@@ -384,7 +403,10 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
|||||||
...entries,
|
...entries,
|
||||||
if (trace.recipient.node != null &&
|
if (trace.recipient.node != null &&
|
||||||
trace.recipient.node!.publicKey != lastKey)
|
trace.recipient.node!.publicKey != lastKey)
|
||||||
_RouteDisplayEntry.fromResolved(trace.recipient),
|
_RouteDisplayEntry.fromResolved(
|
||||||
|
trace.recipient,
|
||||||
|
target: const _RouteEntryTarget.recipient(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,13 +414,26 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
|||||||
final lastKey = pathNodes.last.publicKey;
|
final lastKey = pathNodes.last.publicKey;
|
||||||
return [
|
return [
|
||||||
if (trace.sender.node != null && trace.sender.node!.publicKey != firstKey)
|
if (trace.sender.node != null && trace.sender.node!.publicKey != firstKey)
|
||||||
_RouteDisplayEntry.fromResolved(trace.sender),
|
_RouteDisplayEntry.fromResolved(
|
||||||
|
trace.sender,
|
||||||
|
target: const _RouteEntryTarget.sender(),
|
||||||
|
),
|
||||||
...trace.matchedPathNodes
|
...trace.matchedPathNodes
|
||||||
.where((entry) => entry.node != null)
|
.asMap()
|
||||||
.map(_RouteDisplayEntry.fromResolved),
|
.entries
|
||||||
|
.where((entry) => entry.value.node != null)
|
||||||
|
.map(
|
||||||
|
(entry) => _RouteDisplayEntry.fromResolved(
|
||||||
|
entry.value,
|
||||||
|
target: _RouteEntryTarget.pathNode(entry.key),
|
||||||
|
),
|
||||||
|
),
|
||||||
if (trace.recipient.node != null &&
|
if (trace.recipient.node != null &&
|
||||||
trace.recipient.node!.publicKey != lastKey)
|
trace.recipient.node!.publicKey != lastKey)
|
||||||
_RouteDisplayEntry.fromResolved(trace.recipient),
|
_RouteDisplayEntry.fromResolved(
|
||||||
|
trace.recipient,
|
||||||
|
target: const _RouteEntryTarget.recipient(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,7 +550,7 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
|||||||
if (senderNode.node != null) senderNode,
|
if (senderNode.node != null) senderNode,
|
||||||
...inferred.map(
|
...inferred.map(
|
||||||
(node) => ResolvedTraceNode(
|
(node) => ResolvedTraceNode(
|
||||||
node: node,
|
candidates: [node],
|
||||||
matchCount: 1,
|
matchCount: 1,
|
||||||
usedOnlineFallback: false,
|
usedOnlineFallback: false,
|
||||||
),
|
),
|
||||||
@@ -688,6 +723,37 @@ class _TraceResult {
|
|||||||
required this.pathHashes,
|
required this.pathHashes,
|
||||||
required this.matchedPathNodes,
|
required this.matchedPathNodes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_TraceResult cycleEntry(_RouteEntryTarget target) {
|
||||||
|
switch (target.kind) {
|
||||||
|
case _RouteEntryKind.sender:
|
||||||
|
return _TraceResult(
|
||||||
|
mode: mode,
|
||||||
|
sender: sender.cycle(),
|
||||||
|
recipient: recipient,
|
||||||
|
pathHashes: pathHashes,
|
||||||
|
matchedPathNodes: matchedPathNodes,
|
||||||
|
);
|
||||||
|
case _RouteEntryKind.recipient:
|
||||||
|
return _TraceResult(
|
||||||
|
mode: mode,
|
||||||
|
sender: sender,
|
||||||
|
recipient: recipient.cycle(),
|
||||||
|
pathHashes: pathHashes,
|
||||||
|
matchedPathNodes: matchedPathNodes,
|
||||||
|
);
|
||||||
|
case _RouteEntryKind.pathNode:
|
||||||
|
final updated = matchedPathNodes.toList();
|
||||||
|
updated[target.index] = updated[target.index].cycle();
|
||||||
|
return _TraceResult(
|
||||||
|
mode: mode,
|
||||||
|
sender: sender,
|
||||||
|
recipient: recipient,
|
||||||
|
pathHashes: pathHashes,
|
||||||
|
matchedPathNodes: updated,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RouteDisplayEntry {
|
class _RouteDisplayEntry {
|
||||||
@@ -695,17 +761,22 @@ class _RouteDisplayEntry {
|
|||||||
final String label;
|
final String label;
|
||||||
final String? keyLabel;
|
final String? keyLabel;
|
||||||
final String? matchSummary;
|
final String? matchSummary;
|
||||||
|
final _RouteEntryTarget target;
|
||||||
|
|
||||||
const _RouteDisplayEntry({
|
const _RouteDisplayEntry({
|
||||||
required this.resolved,
|
required this.resolved,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.keyLabel,
|
required this.keyLabel,
|
||||||
required this.matchSummary,
|
required this.matchSummary,
|
||||||
|
required this.target,
|
||||||
});
|
});
|
||||||
|
|
||||||
MeshMapNode? get node => resolved.node;
|
MeshMapNode? get node => resolved.node;
|
||||||
|
|
||||||
factory _RouteDisplayEntry.fromResolved(ResolvedTraceNode resolved) {
|
factory _RouteDisplayEntry.fromResolved(
|
||||||
|
ResolvedTraceNode resolved, {
|
||||||
|
required _RouteEntryTarget target,
|
||||||
|
}) {
|
||||||
final node = resolved.node!;
|
final node = resolved.node!;
|
||||||
return _RouteDisplayEntry(
|
return _RouteDisplayEntry(
|
||||||
resolved: resolved,
|
resolved: resolved,
|
||||||
@@ -715,6 +786,21 @@ class _RouteDisplayEntry {
|
|||||||
math.min(12, node.publicKey.length),
|
math.min(12, node.publicKey.length),
|
||||||
),
|
),
|
||||||
matchSummary: resolved.matchSummary,
|
matchSummary: resolved.matchSummary,
|
||||||
|
target: target,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum _RouteEntryKind { sender, recipient, pathNode }
|
||||||
|
|
||||||
|
class _RouteEntryTarget {
|
||||||
|
final _RouteEntryKind kind;
|
||||||
|
final int index;
|
||||||
|
|
||||||
|
const _RouteEntryTarget._(this.kind, [this.index = 0]);
|
||||||
|
|
||||||
|
const _RouteEntryTarget.sender() : this._(_RouteEntryKind.sender);
|
||||||
|
const _RouteEntryTarget.recipient() : this._(_RouteEntryKind.recipient);
|
||||||
|
const _RouteEntryTarget.pathNode(int index)
|
||||||
|
: this._(_RouteEntryKind.pathNode, index);
|
||||||
|
}
|
||||||
|
|||||||
@@ -795,8 +795,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "0efc820"
|
ref: "461f010"
|
||||||
resolved-ref: "0efc8200a80a6bf8eb2fe305f212cb3c0c0965c4"
|
resolved-ref: "461f010a2f1fcd343357c152228891e295d391be"
|
||||||
url: "https://github.com/dz0ny/meshcore_client.git"
|
url: "https://github.com/dz0ny/meshcore_client.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ dependencies:
|
|||||||
meshcore_client:
|
meshcore_client:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/dz0ny/meshcore_client.git
|
url: https://github.com/dz0ny/meshcore_client.git
|
||||||
ref: "b66e268"
|
ref: "461f010"
|
||||||
|
|
||||||
# Codec2 ultra-low-bitrate speech codec (FFI plugin)
|
# Codec2 ultra-low-bitrate speech codec (FFI plugin)
|
||||||
codec2_flutter:
|
codec2_flutter:
|
||||||
|
|||||||
@@ -61,6 +61,33 @@ void main() {
|
|||||||
expect(resolved.usedOnlineFallback, isTrue);
|
expect(resolved.usedOnlineFallback, isTrue);
|
||||||
expect(resolved.matchCount, 1);
|
expect(resolved.matchCount, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('cycles through ambiguous local prefix matches', () {
|
||||||
|
final first = _node(
|
||||||
|
name: 'First Match',
|
||||||
|
publicKey: 'cc1100',
|
||||||
|
latitude: 46.08,
|
||||||
|
longitude: 14.52,
|
||||||
|
);
|
||||||
|
final second = _node(
|
||||||
|
name: 'Second Match',
|
||||||
|
publicKey: 'cc11ff',
|
||||||
|
latitude: 46.09,
|
||||||
|
longitude: 14.53,
|
||||||
|
);
|
||||||
|
|
||||||
|
final resolved = TraceNodeResolver.resolveBest(
|
||||||
|
nodes: [second, first],
|
||||||
|
localPublicKeys: {first.publicKey, second.publicKey},
|
||||||
|
prefixHex: 'cc11',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved.matchCount, 2);
|
||||||
|
expect(resolved.canCycle, isTrue);
|
||||||
|
expect(resolved.node?.name, 'Second Match');
|
||||||
|
expect(resolved.cycle().node?.name, 'First Match');
|
||||||
|
expect(resolved.cycle().cycle().node?.name, 'Second Match');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
MeshMapNode _node({
|
MeshMapNode _node({
|
||||||
|
|||||||
Reference in New Issue
Block a user