mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-06 16:56:41 +10:00
Enhance contact handling and UI updates across multiple screens
add unfiltered contact access and improve last seen resolution
This commit is contained in:
@@ -328,6 +328,9 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
List<Contact> get allContactsUnfiltered =>
|
||||||
|
List.unmodifiable([..._contacts, ..._discoveredContacts]);
|
||||||
|
|
||||||
List<Contact> get discoveredContacts {
|
List<Contact> get discoveredContacts {
|
||||||
return List.unmodifiable(_discoveredContacts);
|
return List.unmodifiable(_discoveredContacts);
|
||||||
}
|
}
|
||||||
@@ -2372,6 +2375,18 @@ class MeshCoreConnector extends ChangeNotifier {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Contact getFromDiscovered(Contact contact) {
|
||||||
|
final tmp = _discoveredContacts.firstWhere(
|
||||||
|
(c) => c.publicKeyHex == contact.publicKeyHex,
|
||||||
|
orElse: () => contact,
|
||||||
|
);
|
||||||
|
return contact.copyWith(
|
||||||
|
rawPacket: tmp.rawPacket,
|
||||||
|
latitude: tmp.latitude,
|
||||||
|
longitude: tmp.longitude,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> getContacts({int? since, bool preserveExisting = false}) async {
|
Future<void> getContacts({int? since, bool preserveExisting = false}) async {
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,12 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
|||||||
final primaryPath = !channelMessage && !message.isOutgoing
|
final primaryPath = !channelMessage && !message.isOutgoing
|
||||||
? Uint8List.fromList(primaryPathTmp.reversed.toList())
|
? Uint8List.fromList(primaryPathTmp.reversed.toList())
|
||||||
: primaryPathTmp;
|
: primaryPathTmp;
|
||||||
|
<<<<<<< HEAD
|
||||||
final hops = _buildPathHops(primaryPath, connector, l10n);
|
final hops = _buildPathHops(primaryPath, connector, l10n);
|
||||||
|
=======
|
||||||
|
final contacts = connector.allContactsUnfiltered;
|
||||||
|
final hops = _buildPathHops(primaryPath, contacts, l10n);
|
||||||
|
>>>>>>> da74560 (Enhance contact handling and UI updates across multiple screens)
|
||||||
final hasHopDetails = primaryPath.isNotEmpty;
|
final hasHopDetails = primaryPath.isNotEmpty;
|
||||||
final observedLabel = _formatObservedHops(
|
final observedLabel = _formatObservedHops(
|
||||||
primaryPath.length,
|
primaryPath.length,
|
||||||
@@ -385,7 +390,10 @@ class _ChannelMessagePathMapScreenState
|
|||||||
: selectedPathTmp;
|
: selectedPathTmp;
|
||||||
|
|
||||||
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
final selectedIndex = _indexForPath(selectedPath, observedPaths);
|
||||||
final hops = _buildPathHops(selectedPath, connector, context.l10n);
|
final contacts = connector.allContactsUnfiltered
|
||||||
|
.map((c) => connector.getFromDiscovered(c))
|
||||||
|
.toList();
|
||||||
|
final hops = _buildPathHops(selectedPath, contacts, context.l10n);
|
||||||
|
|
||||||
final points = <LatLng>[];
|
final points = <LatLng>[];
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DateTime _resolveLastSeen(Contact contact) {
|
||||||
|
if (contact.type != advTypeChat) return contact.lastSeen;
|
||||||
|
return contact.lastMessageAt.isAfter(contact.lastSeen)
|
||||||
|
? contact.lastMessageAt
|
||||||
|
: contact.lastSeen;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
@@ -108,11 +115,56 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
trailing: Text(
|
// Clamp text scaling in trailing section to prevent overflow while
|
||||||
_formatLastSeen(context, contact.lastSeen),
|
// maintaining accessibility. Primary content (title/subtitle) scales normally.
|
||||||
style: TextStyle(
|
trailing: MediaQuery(
|
||||||
fontSize: 12,
|
data: MediaQuery.of(context).copyWith(
|
||||||
color: Colors.grey[600],
|
textScaler: TextScaler.linear(
|
||||||
|
MediaQuery.textScalerOf(
|
||||||
|
context,
|
||||||
|
).scale(1.0).clamp(1.0, 1.3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 120,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_formatLastSeen(
|
||||||
|
context,
|
||||||
|
_resolveLastSeen(contact),
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (contact.hasLocation)
|
||||||
|
Icon(
|
||||||
|
Icons.location_on,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.grey[400],
|
||||||
|
),
|
||||||
|
if (contact.rawPacket != null)
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
if (contact.rawPacket != null)
|
||||||
|
Icon(
|
||||||
|
Icons.cell_tower,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.grey[400],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|||||||
@@ -139,7 +139,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
builder: (context, connector, settingsService, pathHistory, child) {
|
builder: (context, connector, settingsService, pathHistory, child) {
|
||||||
final tileCache = context.read<MapTileCacheService>();
|
final tileCache = context.read<MapTileCacheService>();
|
||||||
final settings = settingsService.settings;
|
final settings = settingsService.settings;
|
||||||
final allContacts = connector.allContacts;
|
final allContacts = connector.allContacts
|
||||||
|
.map((c) => connector.getFromDiscovered(c))
|
||||||
|
.toList();
|
||||||
|
|
||||||
final contacts = settings.mapShowDiscoveryContacts
|
final contacts = settings.mapShowDiscoveryContacts
|
||||||
? allContacts
|
? allContacts
|
||||||
@@ -2158,10 +2160,10 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
|
|
||||||
void _removePath() {
|
void _removePath() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_pathTrace.removeLast(); // Remove last node from path trace
|
|
||||||
_pathTraceContacts.remove(
|
_pathTraceContacts.remove(
|
||||||
_pathTrace.last,
|
_pathTrace.last,
|
||||||
); // Remove last contact from path trace
|
); // Remove last contact from path trace
|
||||||
|
_pathTrace.removeLast(); // Remove last node from path trace
|
||||||
_points.removeLast(); // Remove last point from points list
|
_points.removeLast(); // Remove last point from points list
|
||||||
_polylines.clear(); // Clear polylines
|
_polylines.clear(); // Clear polylines
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ class PathTraceMapScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||||
static const double _labelZoomThreshold = 8.5;
|
static const double _labelZoomThreshold = 8.5;
|
||||||
|
//miles to meters conversion for filtering out repeaters that are too far from the last known GPS hop to be a likely match, to avoid false matches that throw off the inferred positions of other hops in the path
|
||||||
|
static const double _maxRepeaterMatchDistanceMeters = 40 * 1609.344;
|
||||||
|
|
||||||
StreamSubscription<Uint8List>? _frameSubscription;
|
StreamSubscription<Uint8List>? _frameSubscription;
|
||||||
Timer? _timeoutTimer;
|
Timer? _timeoutTimer;
|
||||||
@@ -268,17 +270,41 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
Map<int, Contact> pathContacts = {};
|
Map<int, Contact> pathContacts = {};
|
||||||
|
Contact lastContact = Contact(
|
||||||
|
path: Uint8List(0),
|
||||||
|
pathLength: 0,
|
||||||
|
publicKey: connector.selfPublicKey ?? Uint8List(0),
|
||||||
|
name: context.l10n.pathTrace_you,
|
||||||
|
type: advTypeChat,
|
||||||
|
latitude: connector.selfLatitude,
|
||||||
|
longitude: connector.selfLongitude,
|
||||||
|
lastSeen: DateTime.now(),
|
||||||
|
);
|
||||||
if (widget.pathContacts != null) {
|
if (widget.pathContacts != null) {
|
||||||
pathContacts = {for (var c in widget.pathContacts!) c.publicKey[0]: c};
|
pathContacts = {for (var c in widget.pathContacts!) c.publicKey[0]: c};
|
||||||
} else {
|
} else {
|
||||||
final contacts = connector.allContacts;
|
final contacts = connector.allContactsUnfiltered
|
||||||
|
.map((c) => connector.getFromDiscovered(c))
|
||||||
|
.toList();
|
||||||
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
||||||
|
if (lastContact.latitude != null &&
|
||||||
|
lastContact.longitude != null &&
|
||||||
|
repeater.hasLocation &&
|
||||||
|
lastContact.hasLocation &&
|
||||||
|
Distance().distance(
|
||||||
|
LatLng(lastContact.latitude!, lastContact.longitude!),
|
||||||
|
LatLng(repeater.latitude!, repeater.longitude!),
|
||||||
|
) >
|
||||||
|
_maxRepeaterMatchDistanceMeters) {
|
||||||
|
return; //skip reapeaters that are far away from the last one with known GPS, to avoid false matches
|
||||||
|
}
|
||||||
for (var repeaterData in pathData) {
|
for (var repeaterData in pathData) {
|
||||||
if (listEquals(
|
if (listEquals(
|
||||||
repeater.publicKey.sublist(0, 1),
|
repeater.publicKey.sublist(0, 1),
|
||||||
Uint8List.fromList([repeaterData]),
|
Uint8List.fromList([repeaterData]),
|
||||||
)) {
|
)) {
|
||||||
pathContacts[repeaterData] = repeater;
|
pathContacts[repeaterData] = repeater;
|
||||||
|
lastContact = repeater;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ class ContactExport {
|
|||||||
final double lon;
|
final double lon;
|
||||||
final String desc;
|
final String desc;
|
||||||
final double? ele;
|
final double? ele;
|
||||||
|
final String url;
|
||||||
ContactExport({
|
ContactExport({
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.lat,
|
required this.lat,
|
||||||
required this.lon,
|
required this.lon,
|
||||||
required this.desc,
|
required this.desc,
|
||||||
|
required this.url,
|
||||||
this.ele,
|
this.ele,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -40,6 +41,7 @@ class GpxExport {
|
|||||||
String name,
|
String name,
|
||||||
double lat,
|
double lat,
|
||||||
double lon,
|
double lon,
|
||||||
|
String url,
|
||||||
String desc, [
|
String desc, [
|
||||||
double? ele,
|
double? ele,
|
||||||
]) {
|
]) {
|
||||||
@@ -50,55 +52,72 @@ class GpxExport {
|
|||||||
lon: lon,
|
lon: lon,
|
||||||
desc: desc.trim(),
|
desc: desc.trim(),
|
||||||
ele: ele,
|
ele: ele,
|
||||||
|
url: url,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addRepeaters() {
|
void addRepeaters() {
|
||||||
final contacts = _connector.contacts
|
final contacts = _connector.allContacts
|
||||||
.where((c) => c.type == advTypeRepeater || c.type == advTypeRoom)
|
.where((c) => c.type == advTypeRepeater || c.type == advTypeRoom)
|
||||||
|
.map((c) => _connector.getFromDiscovered(c))
|
||||||
.toList();
|
.toList();
|
||||||
for (var contact in contacts) {
|
for (var contact in contacts) {
|
||||||
if (contact.latitude == null || contact.longitude == null) {
|
if (contact.latitude == null || contact.longitude == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
final url = contact.rawPacket != null
|
||||||
|
? "meshcore://${pubKeyToHex(contact.rawPacket!)}"
|
||||||
|
: "";
|
||||||
_addContact(
|
_addContact(
|
||||||
contact.name,
|
contact.name,
|
||||||
contact.latitude!,
|
contact.latitude!,
|
||||||
contact.longitude!,
|
contact.longitude!,
|
||||||
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
|
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
|
||||||
|
url,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void addContacts() {
|
void addContacts() {
|
||||||
final contacts = _connector.contacts
|
final contacts = _connector.allContacts
|
||||||
.where((c) => c.type == advTypeChat)
|
.where((c) => c.type == advTypeChat)
|
||||||
|
.map((c) => _connector.getFromDiscovered(c))
|
||||||
.toList();
|
.toList();
|
||||||
for (var contact in contacts) {
|
for (var contact in contacts) {
|
||||||
if (contact.latitude == null || contact.longitude == null) {
|
if (contact.latitude == null || contact.longitude == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
final url = contact.rawPacket != null
|
||||||
|
? "meshcore://${pubKeyToHex(contact.rawPacket!)}"
|
||||||
|
: "";
|
||||||
_addContact(
|
_addContact(
|
||||||
contact.name,
|
contact.name,
|
||||||
contact.latitude!,
|
contact.latitude!,
|
||||||
contact.longitude!,
|
contact.longitude!,
|
||||||
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
|
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
|
||||||
|
url,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAll() {
|
void addAll() {
|
||||||
final contacts = _connector.contacts;
|
final contacts = _connector.allContacts
|
||||||
for (var contact in contacts.toList()) {
|
.map((c) => _connector.getFromDiscovered(c))
|
||||||
|
.toList();
|
||||||
|
for (var contact in contacts) {
|
||||||
if (contact.latitude == null || contact.longitude == null) {
|
if (contact.latitude == null || contact.longitude == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
final url = contact.rawPacket != null
|
||||||
|
? "meshcore://${pubKeyToHex(contact.rawPacket!)}"
|
||||||
|
: "";
|
||||||
_addContact(
|
_addContact(
|
||||||
contact.name,
|
contact.name,
|
||||||
contact.latitude ?? 0.0,
|
contact.latitude ?? 0.0,
|
||||||
contact.longitude ?? 0.0,
|
contact.longitude ?? 0.0,
|
||||||
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
|
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
|
||||||
|
url,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,6 +157,9 @@ class GpxExport {
|
|||||||
ele: c.ele,
|
ele: c.ele,
|
||||||
name: c.name,
|
name: c.name,
|
||||||
desc: c.desc,
|
desc: c.desc,
|
||||||
|
extensions: {
|
||||||
|
"meshcore": {"url": c.url},
|
||||||
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||||
import 'package:meshcore_open/models/path_history.dart';
|
import 'package:meshcore_open/models/path_history.dart';
|
||||||
import 'package:meshcore_open/screens/path_trace_map.dart';
|
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||||
import 'package:meshcore_open/widgets/elements_ui.dart';
|
import 'package:meshcore_open/widgets/elements_ui.dart';
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
|
|||||||
messageBytes: responseBytes,
|
messageBytes: responseBytes,
|
||||||
);
|
);
|
||||||
final timeoutSeconds = (timeoutMs / 1000).ceil();
|
final timeoutSeconds = (timeoutMs / 1000).ceil();
|
||||||
final timeout = Duration(milliseconds: timeoutMs);
|
final timeout = Duration(milliseconds: timeoutMs + 2000);
|
||||||
final selectionLabel = selection.useFlood
|
final selectionLabel = selection.useFlood
|
||||||
? 'flood'
|
? 'flood'
|
||||||
: '${selection.hopCount} hops';
|
: '${selection.hopCount} hops';
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
|
|||||||
messageBytes: responseBytes,
|
messageBytes: responseBytes,
|
||||||
);
|
);
|
||||||
final timeoutSeconds = (timeoutMs / 1000).ceil();
|
final timeoutSeconds = (timeoutMs / 1000).ceil();
|
||||||
final timeout = Duration(milliseconds: timeoutMs);
|
final timeout = Duration(milliseconds: timeoutMs + 2000);
|
||||||
final selectionLabel = selection.useFlood
|
final selectionLabel = selection.useFlood
|
||||||
? 'flood'
|
? 'flood'
|
||||||
: '${selection.hopCount} hops';
|
: '${selection.hopCount} hops';
|
||||||
|
|||||||
Reference in New Issue
Block a user