mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Enhance contact telemetry updates with last seen timestamp and improve message bubble UI for SAR and drawing indicators
This commit is contained in:
@@ -304,10 +304,19 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
debugPrint(' ✅ Parsed new telemetry');
|
debugPrint(' ✅ Parsed new telemetry');
|
||||||
debugPrint(' New telemetry timestamp: ${telemetry.timestamp}');
|
debugPrint(' New telemetry timestamp: ${telemetry.timestamp}');
|
||||||
|
|
||||||
// Update contact with new telemetry
|
// Update contact with new telemetry AND last seen time
|
||||||
final updatedContact = contact.copyWith(telemetry: telemetry);
|
// lastAdvert is Unix timestamp in seconds
|
||||||
|
final currentTimestamp =
|
||||||
|
(DateTime.now().millisecondsSinceEpoch / 1000).round();
|
||||||
|
debugPrint(' Old lastAdvert: ${contact.lastAdvert}');
|
||||||
|
debugPrint(' New lastAdvert: $currentTimestamp');
|
||||||
|
|
||||||
|
final updatedContact = contact.copyWith(
|
||||||
|
telemetry: telemetry,
|
||||||
|
lastAdvert: currentTimestamp, // Update last seen time
|
||||||
|
);
|
||||||
_contacts[contact.publicKeyHex] = updatedContact;
|
_contacts[contact.publicKeyHex] = updatedContact;
|
||||||
debugPrint(' ✅ Updated contact in map');
|
debugPrint(' ✅ Updated contact in map (with new lastAdvert)');
|
||||||
|
|
||||||
_persistContacts();
|
_persistContacts();
|
||||||
debugPrint(' ✅ Persisted contacts to storage');
|
debugPrint(' ✅ Persisted contacts to storage');
|
||||||
|
|||||||
@@ -1413,14 +1413,107 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Header: Sender and time
|
// Header: Badge (if SAR or drawing) and time
|
||||||
|
if (isSarMarker || message.isDrawing)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// Unread indicator badge
|
||||||
|
if (!message.isRead &&
|
||||||
|
!message.isSentMessage &&
|
||||||
|
!message.isSystemMessage &&
|
||||||
|
!isSarMarker)
|
||||||
|
Container(
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
margin: const EdgeInsets.only(right: 8),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.blue,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isSarMarker)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getSarMarkerBorderColor(context, isDarkMode),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.warning_amber_rounded,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
AppLocalizations.of(context)!.sarAlert,
|
||||||
|
style: Theme.of(context).textTheme.labelSmall
|
||||||
|
?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (message.isDrawing)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.draw,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
AppLocalizations.of(context)!.mapDrawing,
|
||||||
|
style: Theme.of(context).textTheme.labelSmall
|
||||||
|
?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
message.getLocalizedTimeAgo(context),
|
||||||
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
|
fontWeight: isSarMarker
|
||||||
|
? FontWeight.w600
|
||||||
|
: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Sender info row (shown for all messages)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
// Unread indicator badge
|
// Unread indicator badge (only for regular messages, not SAR/drawing)
|
||||||
if (!message.isRead &&
|
if (!message.isRead &&
|
||||||
!message.isSentMessage &&
|
!message.isSentMessage &&
|
||||||
!message.isSystemMessage &&
|
!message.isSystemMessage &&
|
||||||
!isSarMarker)
|
!isSarMarker &&
|
||||||
|
!message.isDrawing)
|
||||||
Container(
|
Container(
|
||||||
width: 8,
|
width: 8,
|
||||||
height: 8,
|
height: 8,
|
||||||
@@ -1430,122 +1523,56 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isSarMarker)
|
if (isOwnMessage)
|
||||||
Container(
|
Icon(
|
||||||
padding: const EdgeInsets.symmetric(
|
Icons.account_circle,
|
||||||
horizontal: 10,
|
size: 16,
|
||||||
vertical: 4,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: _getSarMarkerBorderColor(context, isDarkMode),
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.warning_amber_rounded,
|
|
||||||
size: 16,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
AppLocalizations.of(context)!.sarAlert,
|
|
||||||
style: Theme.of(context).textTheme.labelSmall
|
|
||||||
?.copyWith(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
else if (message.isDrawing)
|
else if (message.isChannelMessage)
|
||||||
Container(
|
const Icon(Icons.tag, size: 16)
|
||||||
padding: const EdgeInsets.symmetric(
|
else
|
||||||
horizontal: 10,
|
const Icon(Icons.person, size: 16),
|
||||||
vertical: 4,
|
const SizedBox(width: 4),
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.draw,
|
|
||||||
size: 16,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
AppLocalizations.of(context)!.mapDrawing,
|
|
||||||
style: Theme.of(context).textTheme.labelSmall
|
|
||||||
?.copyWith(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else ...[
|
|
||||||
if (isOwnMessage)
|
|
||||||
Icon(
|
|
||||||
Icons.account_circle,
|
|
||||||
size: 16,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
)
|
|
||||||
else if (message.isChannelMessage)
|
|
||||||
const Icon(Icons.tag, size: 16)
|
|
||||||
else
|
|
||||||
const Icon(Icons.person, size: 16),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
displayName,
|
|
||||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: isOwnMessage
|
|
||||||
? Theme.of(context).colorScheme.primary
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Show recipient for sent direct messages
|
|
||||||
if (isOwnMessage &&
|
|
||||||
message.isContactMessage &&
|
|
||||||
recipientDisplayName != null) ...[
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward,
|
|
||||||
size: 14,
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).textTheme.labelSmall?.color?.withValues(alpha: 0.6),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
recipientDisplayName,
|
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).textTheme.labelSmall?.color?.withValues(alpha: 0.7),
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
const Spacer(),
|
|
||||||
Text(
|
Text(
|
||||||
message.getLocalizedTimeAgo(context),
|
displayName,
|
||||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||||
fontWeight: isSarMarker
|
fontWeight: FontWeight.bold,
|
||||||
? FontWeight.w600
|
color: isOwnMessage
|
||||||
: FontWeight.normal,
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Show recipient for sent direct messages
|
||||||
|
if (isOwnMessage &&
|
||||||
|
message.isContactMessage &&
|
||||||
|
recipientDisplayName != null) ...[
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Icon(
|
||||||
|
Icons.arrow_forward,
|
||||||
|
size: 14,
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.labelSmall?.color?.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
recipientDisplayName,
|
||||||
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.labelSmall?.color?.withValues(alpha: 0.7),
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const Spacer(),
|
||||||
|
// Time for regular messages (not shown for SAR/drawing as it's already above)
|
||||||
|
if (!isSarMarker && !message.isDrawing)
|
||||||
|
Text(
|
||||||
|
message.getLocalizedTimeAgo(context),
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -1663,14 +1690,6 @@ class _MessageBubble extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (drawing.senderName != null) ...[
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
'From: ${drawing.senderName}',
|
|
||||||
style: Theme.of(context).textTheme.labelSmall
|
|
||||||
?.copyWith(fontStyle: FontStyle.italic),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -148,16 +148,16 @@ void main() {
|
|||||||
|
|
||||||
// Verify 3-byte signed encoding
|
// Verify 3-byte signed encoding
|
||||||
// Lat: -33.8688 * 10000 = -338688
|
// Lat: -33.8688 * 10000 = -338688
|
||||||
// In 24-bit two's complement: -338688 + 0x1000000 = 16438608 = 0xFAD4E0
|
// In 24-bit two's complement: -338688 + 0x1000000 = 16438528 = 0xFAD500
|
||||||
final latEncoded =
|
final latEncoded =
|
||||||
(encoded[2] << 16) | (encoded[3] << 8) | encoded[4];
|
(encoded[2] << 16) | (encoded[3] << 8) | encoded[4];
|
||||||
// Lon: -151.2093 * 10000 = -1512093
|
// Lon: -151.2093 * 10000 = -1512093
|
||||||
// In 24-bit two's complement: -1512093 + 0x1000000 = 15265123 = 0xE8E963
|
// In 24-bit two's complement: -1512093 + 0x1000000 = 15265123 = 0xE8ED63
|
||||||
final lonEncoded =
|
final lonEncoded =
|
||||||
(encoded[5] << 16) | (encoded[6] << 8) | encoded[7];
|
(encoded[5] << 16) | (encoded[6] << 8) | encoded[7];
|
||||||
|
|
||||||
expect(latEncoded, equals(0xFAD4E0)); // Verify two's complement
|
expect(latEncoded, equals(0xFAD500)); // Verify two's complement
|
||||||
expect(lonEncoded, equals(0xE8E963));
|
expect(lonEncoded, equals(0xE8ED63));
|
||||||
|
|
||||||
// Verify decoding
|
// Verify decoding
|
||||||
final decoded = CayenneLppParser.parse(encoded);
|
final decoded = CayenneLppParser.parse(encoded);
|
||||||
|
|||||||
Reference in New Issue
Block a user