From a6fbc10221462a4d1205fa5e7e60942ba35b2587 Mon Sep 17 00:00:00 2001 From: Janez T Date: Sun, 26 Oct 2025 17:04:25 +0100 Subject: [PATCH] feat: Enhance contact telemetry updates with last seen timestamp and improve message bubble UI for SAR and drawing indicators --- lib/providers/contacts_provider.dart | 15 +- lib/screens/messages_tab.dart | 265 +++++++++++---------- test/services/cayenne_lpp_parser_test.dart | 8 +- 3 files changed, 158 insertions(+), 130 deletions(-) diff --git a/lib/providers/contacts_provider.dart b/lib/providers/contacts_provider.dart index c505b48..5ef4b54 100644 --- a/lib/providers/contacts_provider.dart +++ b/lib/providers/contacts_provider.dart @@ -304,10 +304,19 @@ class ContactsProvider with ChangeNotifier { debugPrint(' ✅ Parsed new telemetry'); debugPrint(' New telemetry timestamp: ${telemetry.timestamp}'); - // Update contact with new telemetry - final updatedContact = contact.copyWith(telemetry: telemetry); + // Update contact with new telemetry AND last seen time + // 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; - debugPrint(' ✅ Updated contact in map'); + debugPrint(' ✅ Updated contact in map (with new lastAdvert)'); _persistContacts(); debugPrint(' ✅ Persisted contacts to storage'); diff --git a/lib/screens/messages_tab.dart b/lib/screens/messages_tab.dart index 09aea9f..6f98b26 100644 --- a/lib/screens/messages_tab.dart +++ b/lib/screens/messages_tab.dart @@ -1413,14 +1413,107 @@ class _MessageBubble extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, 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( children: [ - // Unread indicator badge + // Unread indicator badge (only for regular messages, not SAR/drawing) if (!message.isRead && !message.isSentMessage && !message.isSystemMessage && - !isSarMarker) + !isSarMarker && + !message.isDrawing) Container( width: 8, height: 8, @@ -1430,122 +1523,56 @@ class _MessageBubble extends StatelessWidget { 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, - ), - ), - ], - ), + if (isOwnMessage) + Icon( + Icons.account_circle, + size: 16, + color: Theme.of(context).colorScheme.primary, ) - 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, - ), - ), - ], - ), - ) - 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(), + else if (message.isChannelMessage) + const Icon(Icons.tag, size: 16) + else + const Icon(Icons.person, size: 16), + const SizedBox(width: 4), Text( - message.getLocalizedTimeAgo(context), - style: Theme.of(context).textTheme.labelSmall?.copyWith( - fontWeight: isSarMarker - ? FontWeight.w600 - : FontWeight.normal, + 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(), + // 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), @@ -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), - ), - ], ], ), ), diff --git a/test/services/cayenne_lpp_parser_test.dart b/test/services/cayenne_lpp_parser_test.dart index 2b130d5..731e14b 100644 --- a/test/services/cayenne_lpp_parser_test.dart +++ b/test/services/cayenne_lpp_parser_test.dart @@ -148,16 +148,16 @@ void main() { // Verify 3-byte signed encoding // 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 = (encoded[2] << 16) | (encoded[3] << 8) | encoded[4]; // 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 = (encoded[5] << 16) | (encoded[6] << 8) | encoded[7]; - expect(latEncoded, equals(0xFAD4E0)); // Verify two's complement - expect(lonEncoded, equals(0xE8E963)); + expect(latEncoded, equals(0xFAD500)); // Verify two's complement + expect(lonEncoded, equals(0xE8ED63)); // Verify decoding final decoded = CayenneLppParser.parse(encoded);