From 573921e002b1dffb3c782f022b0f4d17cd697cdd Mon Sep 17 00:00:00 2001 From: Janez T Date: Sun, 5 Apr 2026 19:54:48 +0200 Subject: [PATCH] fix: Clarify shared location labels and onboarding copy #123 --- lib/l10n/app_localizations.dart | 4 +- lib/models/message_contact_location.dart | 8 +- lib/widgets/messages/message_bubble.dart | 315 +++++++++++++---------- test/widgets/message_bubble_test.dart | 54 +++- 4 files changed, 237 insertions(+), 144 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index c88a61a..2cddd4b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3267,7 +3267,7 @@ abstract class AppLocalizations { /// Updated onboarding overview feature 2 /// /// In en, this message translates to: - /// **'Share SAR markers, map drawings, voice clips, and images over the mesh.'** + /// **'Share live location to a private channel, plus SAR markers, map drawings, voice clips, and images over the mesh.'** String get wizardOverviewFeature2; /// Updated onboarding overview feature 3 @@ -3459,7 +3459,7 @@ abstract class AppLocalizations { /// Updated onboarding map feature 1 /// /// In en, this message translates to: - /// **'Track your own position, teammate locations, and movement trails on the map.'** + /// **'Track your own position, teammate locations, and share live location to a private channel from the channel menu.'** String get wizardMapOpsFeature1; /// Updated onboarding map feature 2 diff --git a/lib/models/message_contact_location.dart b/lib/models/message_contact_location.dart index 933e26d..7de6027 100644 --- a/lib/models/message_contact_location.dart +++ b/lib/models/message_contact_location.dart @@ -15,10 +15,14 @@ class MessageContactLocation { String get technicalSourceLabel { switch (source) { + case 'shared': + return 'shared location'; + case 'gps': + return 'shared location'; case 'telemetry': - return 'telemetry'; + return 'live telemetry'; case 'advert': - return 'advert'; + return 'shared advert'; default: return source; } diff --git a/lib/widgets/messages/message_bubble.dart b/lib/widgets/messages/message_bubble.dart index 1ce7bc7..3fe87d6 100644 --- a/lib/widgets/messages/message_bubble.dart +++ b/lib/widgets/messages/message_bubble.dart @@ -22,7 +22,9 @@ import '../../providers/image_provider.dart' as ip; import '../../providers/app_provider.dart'; import '../drawing_minimap_preview.dart'; import '../../models/ble_packet_log.dart'; +import '../../models/message_contact_location.dart'; import '../../services/sar_template_service.dart'; +import '../../services/location_tracking_service.dart'; import '../../utils/toast_logger.dart'; import '../../utils/sar_message_parser.dart'; import '../../utils/key_comparison.dart'; @@ -421,6 +423,8 @@ class _MessageBubbleState extends State { } void _showMessageOptions(BuildContext context) { + final parentContext = context; + final l10n = AppLocalizations.of(context)!; final connectionProvider = context.read(); final selfPublicKey = connectionProvider.deviceInfo.publicKey; final isOwnMessage = @@ -433,7 +437,7 @@ class _MessageBubbleState extends State { shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), - builder: (context) => Padding( + builder: (sheetContext) => Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Column( mainAxisSize: MainAxisSize.min, @@ -441,29 +445,29 @@ class _MessageBubbleState extends State { if (!isOwnMessage && widget.onReply != null) ListTile( leading: Icon(Icons.reply), - title: Text(AppLocalizations.of(context)!.reply), + title: Text(l10n.reply), onTap: () { - Navigator.pop(context); + Navigator.pop(sheetContext); widget.onReply?.call(); }, ), // Copy text option ListTile( leading: Icon(Icons.copy), - title: Text(AppLocalizations.of(context)!.copyText), + title: Text(l10n.copyText), onTap: () { Clipboard.setData(ClipboardData(text: widget.message.text)); - Navigator.pop(context); + Navigator.pop(sheetContext); ToastLogger.success( - context, - AppLocalizations.of(context)!.textCopiedToClipboard, + parentContext, + l10n.textCopiedToClipboard, ); }, ), // Save as Template option (only for SAR markers without existing template) if (widget.message.isSarMarker) Builder( - builder: (context) { + builder: (_) { // Extract emoji from SAR message final sarInfo = SarMessageParser.parse(widget.message.text); if (sarInfo == null || sarInfo.emoji.isEmpty) { @@ -482,10 +486,10 @@ class _MessageBubbleState extends State { return ListTile( leading: Icon(Icons.bookmark_add), - title: Text(AppLocalizations.of(context)!.saveAsTemplate), + title: Text(l10n.saveAsTemplate), onTap: () { - Navigator.pop(context); - _saveAsTemplate(context); + Navigator.pop(sheetContext); + _saveAsTemplate(parentContext); }, ); }, @@ -495,49 +499,49 @@ class _MessageBubbleState extends State { widget.message.sarGpsCoordinates != null) ListTile( leading: Icon(Icons.share_location), - title: Text(AppLocalizations.of(context)!.shareLocation), + title: Text(l10n.shareLocation), onTap: () { - Navigator.pop(context); - _shareLocation(context); + Navigator.pop(sheetContext); + _shareLocation(parentContext); }, ), // Navigate to drawing option (only for drawing messages) if (widget.message.isDrawing && widget.message.drawingId != null) ListTile( leading: Icon(Icons.map), - title: Text(AppLocalizations.of(context)!.navigateToDrawing), + title: Text(l10n.navigateToDrawing), onTap: () { - Navigator.pop(context); - _navigateToDrawing(context); + Navigator.pop(sheetContext); + _navigateToDrawing(parentContext); }, ), // Copy coordinates option (only for drawing messages) if (widget.message.isDrawing && widget.message.drawingId != null) ListTile( leading: Icon(Icons.copy), - title: Text(AppLocalizations.of(context)!.copyCoordinates), + title: Text(l10n.copyCoordinates), onTap: () { - Navigator.pop(context); - _copyDrawingCoordinates(context); + Navigator.pop(sheetContext); + _copyDrawingCoordinates(parentContext); }, ), // Hide from map option (only for drawing messages) if (widget.message.isDrawing && widget.message.drawingId != null) ListTile( leading: Icon(Icons.visibility_off), - title: Text(AppLocalizations.of(context)!.hideFromMap), + title: Text(l10n.hideFromMap), onTap: () { - Navigator.pop(context); - _hideDrawingFromMap(context); + Navigator.pop(sheetContext); + _hideDrawingFromMap(parentContext); }, ), // Technical details option ListTile( leading: Icon(Icons.data_object), - title: Text(AppLocalizations.of(context)!.technicalDetails), + title: Text(l10n.technicalDetails), onTap: () { - Navigator.pop(context); - _showTechnicalDetails(context); + Navigator.pop(sheetContext); + _showTechnicalDetails(parentContext); }, ), if (!isOwnMessage && @@ -545,22 +549,22 @@ class _MessageBubbleState extends State { widget.message.pathLen < 255) ListTile( leading: Icon(Icons.route), - title: Text(AppLocalizations.of(context)!.trace), + title: Text(l10n.trace), onTap: () { - Navigator.pop(context); - _showTraceSheet(context); + Navigator.pop(sheetContext); + _showTraceSheet(parentContext); }, ), // Delete message option ListTile( leading: Icon(Icons.delete, color: Colors.red), title: Text( - AppLocalizations.of(context)!.delete, + l10n.delete, style: const TextStyle(color: Colors.red), ), onTap: () { - Navigator.pop(context); - _showDeleteConfirmation(context); + Navigator.pop(sheetContext); + _showDeleteConfirmation(parentContext); }, ), ], @@ -582,6 +586,7 @@ class _MessageBubbleState extends State { } void _showTechnicalDetails(BuildContext context) { + final l10n = AppLocalizations.of(context)!; final connectionProvider = context.read(); final radioBw = connectionProvider.deviceInfo.radioBw; final radioSf = connectionProvider.deviceInfo.radioSf; @@ -596,6 +601,7 @@ class _MessageBubbleState extends State { widget.message.isFromSelf(selfPublicKey); String? senderName; + Contact? senderContact; if (widget.message.senderPublicKeyPrefix != null) { final senderKeyHex = widget.message.senderPublicKeyPrefix! .sublist( @@ -606,7 +612,7 @@ class _MessageBubbleState extends State { ) .map((b) => b.toRadixString(16).padLeft(2, '0')) .join(''); - final senderContact = contactsProvider.contacts + senderContact = contactsProvider.contacts .where((c) => c.publicKeyHex.startsWith(senderKeyHex)) .firstOrNull; senderName = senderContact?.advName; @@ -629,7 +635,7 @@ class _MessageBubbleState extends State { recipientName = recipientContact?.advName; } - final messageLocationSnapshot = messagesProvider.getMessageContactLocation( + final storedLocationSnapshot = messagesProvider.getMessageContactLocation( widget.message.id, ); final receptionDetails = messagesProvider.getMessageReceptionDetails( @@ -710,6 +716,12 @@ class _MessageBubbleState extends State { final routeMetadata = context .read() .getMessageRouteMetadata(widget.message.id); + final messageLocationSnapshot = _resolveDetailsLocationSnapshot( + storedSnapshot: storedLocationSnapshot, + senderContact: senderContact, + connectionProvider: connectionProvider, + isOwnMessage: isOwnMessage, + ); final rawLines = [ 'Message ID: ${widget.message.id}', @@ -834,12 +846,9 @@ class _MessageBubbleState extends State { } } - void copyField(String value) { + void copyField(BuildContext feedbackContext, String value) { Clipboard.setData(ClipboardData(text: value)); - ToastLogger.success( - context, - AppLocalizations.of(context)!.textCopiedToClipboard, - ); + ToastLogger.success(feedbackContext, l10n.textCopiedToClipboard); } showModalBottomSheet( @@ -861,14 +870,14 @@ class _MessageBubbleState extends State { children: [ Expanded( child: Text( - AppLocalizations.of(context)!.messageTechnicalDetails, + l10n.messageTechnicalDetails, style: Theme.of(sheetContext).textTheme.titleLarge, ), ), IconButton( onPressed: () => Navigator.pop(sheetContext), icon: Icon(Icons.close), - tooltip: AppLocalizations.of(context)!.close, + tooltip: l10n.close, ), ], ), @@ -913,7 +922,7 @@ class _MessageBubbleState extends State { _techSection( sheetContext, icon: Icons.location_on, - title: AppLocalizations.of(context)!.location, + title: l10n.location, child: Column( children: [ _buildLocationPreviewMap( @@ -923,29 +932,29 @@ class _MessageBubbleState extends State { const SizedBox(height: 12), _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.coordinates, + label: l10n.coordinates, value: messageLocationSnapshot .formattedCoordinates, onCopy: () => copyField( + sheetContext, messageLocationSnapshot.formattedCoordinates, ), ), _detailRow( sheetContext, - label: AppLocalizations.of(context)!.source, + label: l10n.source, value: messageLocationSnapshot .technicalSourceLabel, ), _detailRow( sheetContext, - label: AppLocalizations.of(context)!.captured, + label: l10n.captured, value: _formatRfc3339( messageLocationSnapshot.sourceTimestamp ?? messageLocationSnapshot.capturedAt, ), onCopy: () => copyField( + sheetContext, _formatRfc3339( messageLocationSnapshot.sourceTimestamp ?? messageLocationSnapshot.capturedAt, @@ -963,13 +972,13 @@ class _MessageBubbleState extends State { _techSection( sheetContext, icon: Icons.network_check, - title: AppLocalizations.of(context)!.linkQuality, + title: l10n.linkQuality, child: Column( children: [ if (rssiDbm != null) _signalRow( sheetContext, - label: AppLocalizations.of(context)!.rssi, + label: l10n.rssi, valueLabel: '$rssiDbm dBm', normalized: ((rssiDbm.toDouble() + 120.0) / 70.0) @@ -984,7 +993,7 @@ class _MessageBubbleState extends State { SizedBox(height: 8), _signalRow( sheetContext, - label: AppLocalizations.of(context)!.snr, + label: l10n.snr, valueLabel: '${snrDb.toStringAsFixed(1)} dB', normalized: ((snrDb + 20.0) / 40.0).clamp( 0.0, @@ -1005,39 +1014,34 @@ class _MessageBubbleState extends State { _techSection( sheetContext, icon: Icons.tune, - title: AppLocalizations.of(context)!.delivery, + title: l10n.delivery, child: Column( children: [ _detailRow( sheetContext, - label: AppLocalizations.of(context)!.status, + label: l10n.status, value: widget.message.deliveryStatus.name, ), _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.receivedRfc3339, + label: l10n.receivedRfc3339, value: _formatRfc3339(widget.message.receivedAt), onCopy: () => copyField( + sheetContext, _formatRfc3339(widget.message.receivedAt), ), ), if (widget.message.expectedAckTag != null) _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.expectedAckTag, + label: l10n.expectedAckTag, value: widget.message.expectedAckTag! .toString(), ), if (receptionDetails?.senderToReceiptMs != null) _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.senderToReceipt, + label: l10n.senderToReceipt, value: _formatDurationMs( receptionDetails!.senderToReceiptMs!, ), @@ -1045,9 +1049,7 @@ class _MessageBubbleState extends State { if (receptionDetails?.estimatedTransmitMs != null) _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.estimatedTx, + label: l10n.estimatedTx, value: _formatDurationMs( receptionDetails!.estimatedTransmitMs!, ), @@ -1055,9 +1057,7 @@ class _MessageBubbleState extends State { if (receptionDetails?.postTransmitDelayMs != null) _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.posttxDelay, + label: l10n.posttxDelay, value: _formatDurationMs( receptionDetails!.postTransmitDelayMs!, ), @@ -1065,87 +1065,80 @@ class _MessageBubbleState extends State { if (widget.receivedCopies > 1) _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.receivedCopies, + label: l10n.receivedCopies, value: '${widget.receivedCopies}', ), if (widget.message.suggestedTimeoutMs != null) _detailRow( sheetContext, - label: AppLocalizations.of(context)!.ackTimeout, + label: l10n.ackTimeout, value: '${widget.message.suggestedTimeoutMs} ms', ), if (retryCause != null) _detailRow( sheetContext, - label: AppLocalizations.of(context)!.retryCause, + label: l10n.retryCause, value: retryCause, ), if (retryMode != null) _detailRow( sheetContext, - label: AppLocalizations.of(context)!.retryMode, + label: l10n.retryMode, value: retryMode, ), if (widget.message.roundTripTimeMs != null) _detailRow( sheetContext, - label: AppLocalizations.of(context)!.roundTrip, + label: l10n.roundTrip, value: '${widget.message.roundTripTimeMs} ms', ), if (widget.message.retryAttempt > 0) _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.retryAttempt, + label: l10n.retryAttempt, value: '${widget.message.retryAttempt}/4', ), if (widget.message.lastRetryAt != null) _detailRow( sheetContext, - label: AppLocalizations.of(context)!.lastRetry, + label: l10n.lastRetry, value: _formatRfc3339( widget.message.lastRetryAt!, ), onCopy: () => copyField( + sheetContext, _formatRfc3339(widget.message.lastRetryAt!), ), ), if (widget.message.usedFloodFallback) _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.floodFallback, - value: AppLocalizations.of(context)!.yes, + label: l10n.floodFallback, + value: l10n.yes, ), if (routeMetadata?.canonicalPath case final routePath?) _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.selectedPath, + label: l10n.selectedPath, value: routePath, - onCopy: () => copyField(routePath), + onCopy: () => + copyField(sheetContext, routePath), ), if (retryResult != null) _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.retryResult, + label: l10n.retryResult, value: retryResult, ), if (packetPathHex != null) _detailRow( sheetContext, - label: AppLocalizations.of(context)!.pathBytes, + label: l10n.pathBytes, value: packetPathHex, - onCopy: () => copyField(packetPathHex), + onCopy: () => + copyField(sheetContext, packetPathHex), ), ], ), @@ -1154,18 +1147,19 @@ class _MessageBubbleState extends State { _techSection( sheetContext, icon: Icons.badge, - title: AppLocalizations.of(context)!.identity, + title: l10n.identity, child: Column( children: [ _detailRow( sheetContext, - label: AppLocalizations.of(context)!.messageId, + label: l10n.messageId, value: widget.message.id, - onCopy: () => copyField(widget.message.id), + onCopy: () => + copyField(sheetContext, widget.message.id), ), _detailRow( sheetContext, - label: AppLocalizations.of(context)!.sender, + label: l10n.sender, value: senderName ?? widget.message.senderName ?? @@ -1174,24 +1168,26 @@ class _MessageBubbleState extends State { if (senderPrefixHex != null) _detailRow( sheetContext, - label: AppLocalizations.of(context)!.senderKey, + label: l10n.senderKey, value: senderPrefixHex, - onCopy: () => copyField(senderPrefixHex), + onCopy: () => + copyField(sheetContext, senderPrefixHex), ), if (recipientName != null) _detailRow( sheetContext, - label: AppLocalizations.of(context)!.recipient, + label: l10n.recipient, value: recipientName, ), if (recipientPrefixHex != null) _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.recipientKey, + label: l10n.recipientKey, value: recipientPrefixHex, - onCopy: () => copyField(recipientPrefixHex), + onCopy: () => copyField( + sheetContext, + recipientPrefixHex, + ), ), ], ), @@ -1201,53 +1197,47 @@ class _MessageBubbleState extends State { _techSection( sheetContext, icon: Icons.graphic_eq, - title: AppLocalizations.of(context)!.voice, + title: l10n.voice, child: Column( children: [ _detailRow( sheetContext, - label: AppLocalizations.of(context)!.voiceId, + label: l10n.voiceId, value: widget.message.voiceId ?? '-', ), _detailRow( sheetContext, - label: AppLocalizations.of(context)!.envelope, + label: l10n.envelope, value: envelope != null ? 'VE3 compact' - : AppLocalizations.of(context)!.unknown, + : l10n.unknown, ), if (voiceSession != null) _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.sessionProgress, + label: l10n.sessionProgress, value: '${voiceSession.receivedCount}/${voiceSession.total} segments', ), if (voiceSession != null) _detailRow( sheetContext, - label: AppLocalizations.of(context)!.complete, + label: l10n.complete, value: voiceSession.isComplete - ? AppLocalizations.of(context)!.yes - : AppLocalizations.of(context)!.no, + ? l10n.yes + : l10n.no, ), if (transferDetails != null) _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.transfers, + label: l10n.transfers, value: '${transferDetails.totalTransfers}', ), if (transferDetails != null && transferDetails.downloaders.isNotEmpty) _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.downloadedBy, + label: l10n.downloadedBy, value: _formatDownloaderSummary( transferDetails, ), @@ -1255,9 +1245,7 @@ class _MessageBubbleState extends State { if (voiceTxEstimate > Duration.zero) _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.estimatedTx, + label: l10n.estimatedTx, value: voiceTxEstimate.inSeconds < 60 ? '~${voiceTxEstimate.inSeconds}s' : '~${voiceTxEstimate.inMinutes}m ${voiceTxEstimate.inSeconds % 60}s', @@ -1271,28 +1259,28 @@ class _MessageBubbleState extends State { _techSection( sheetContext, icon: Icons.image_outlined, - title: AppLocalizations.of(context)!.image, + title: l10n.image, child: Column( children: [ _detailRow( sheetContext, - label: AppLocalizations.of(context)!.envelope, + label: l10n.envelope, value: 'IE1', ), _detailRow( sheetContext, - label: AppLocalizations.of(context)!.format, + label: l10n.format, value: imageEnvelope.format.label, ), _detailRow( sheetContext, - label: AppLocalizations.of(context)!.dimensions, + label: l10n.dimensions, value: '${imageEnvelope.width}×${imageEnvelope.height}', ), _detailRow( sheetContext, - label: AppLocalizations.of(context)!.segments, + label: l10n.segments, value: imageSession != null ? '${imageSession.receivedCount}/${imageSession.total}' : '${imageEnvelope.total}', @@ -1300,26 +1288,22 @@ class _MessageBubbleState extends State { if (imageSession != null) _detailRow( sheetContext, - label: AppLocalizations.of(context)!.complete, + label: l10n.complete, value: imageSession.isComplete - ? AppLocalizations.of(context)!.yes - : AppLocalizations.of(context)!.no, + ? l10n.yes + : l10n.no, ), if (transferDetails != null) _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.transfers, + label: l10n.transfers, value: '${transferDetails.totalTransfers}', ), if (transferDetails != null && transferDetails.downloaders.isNotEmpty) _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.downloadedBy, + label: l10n.downloadedBy, value: _formatDownloaderSummary( transferDetails, ), @@ -1327,9 +1311,7 @@ class _MessageBubbleState extends State { if (imageTxEstimate > Duration.zero) _detailRow( sheetContext, - label: AppLocalizations.of( - context, - )!.estimatedTx, + label: l10n.estimatedTx, value: imageTxEstimate.inSeconds < 60 ? '~${imageTxEstimate.inSeconds}s' : '~${imageTxEstimate.inMinutes}m ${imageTxEstimate.inSeconds % 60}s', @@ -1344,7 +1326,7 @@ class _MessageBubbleState extends State { dense: true, visualDensity: VisualDensity.compact, title: Text( - AppLocalizations.of(context)!.rawDump, + l10n.rawDump, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, @@ -1429,6 +1411,61 @@ class _MessageBubbleState extends State { ); } + MessageContactLocation? _resolveDetailsLocationSnapshot({ + required MessageContactLocation? storedSnapshot, + required Contact? senderContact, + required ConnectionProvider connectionProvider, + required bool isOwnMessage, + }) { + if (storedSnapshot != null) { + return storedSnapshot; + } + + if (isOwnMessage) { + final advLat = connectionProvider.deviceInfo.advLat; + final advLon = connectionProvider.deviceInfo.advLon; + if (advLat != null && + advLon != null && + (advLat != 0 || advLon != 0)) { + return MessageContactLocation( + location: LatLng(advLat / 1e6, advLon / 1e6), + source: 'shared', + capturedAt: widget.message.sentAt, + sourceTimestamp: widget.message.sentAt, + ); + } + + final currentPosition = LocationTrackingService().currentPosition; + if (currentPosition != null) { + return MessageContactLocation( + location: LatLng( + currentPosition.latitude, + currentPosition.longitude, + ), + source: 'shared', + capturedAt: currentPosition.timestamp, + sourceTimestamp: currentPosition.timestamp, + ); + } + } + + final contactLocation = senderContact?.displayLocation; + if (contactLocation != null) { + final source = + senderContact?.telemetry?.gpsLocation != null + ? 'telemetry' + : 'advert'; + return MessageContactLocation( + location: contactLocation, + source: source, + capturedAt: senderContact?.locationUpdateTime ?? widget.message.receivedAt, + sourceTimestamp: senderContact?.locationUpdateTime, + ); + } + + return null; + } + Widget _techSection( BuildContext context, { required IconData icon, diff --git a/test/widgets/message_bubble_test.dart b/test/widgets/message_bubble_test.dart index e2a28eb..087d4e8 100644 --- a/test/widgets/message_bubble_test.dart +++ b/test/widgets/message_bubble_test.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart' as flutter_map; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:geolocator/geolocator.dart'; import 'package:meshcore_sar_app/l10n/app_localizations.dart'; import 'package:meshcore_sar_app/models/message.dart'; import 'package:meshcore_sar_app/models/message_contact_location.dart'; @@ -14,6 +15,7 @@ import 'package:meshcore_sar_app/providers/drawing_provider.dart'; import 'package:meshcore_sar_app/providers/image_provider.dart' as ip; import 'package:meshcore_sar_app/providers/messages_provider.dart'; import 'package:meshcore_sar_app/providers/voice_provider.dart'; +import 'package:meshcore_sar_app/services/location_tracking_service.dart'; import 'package:meshcore_sar_app/services/voice_codec_service.dart'; import 'package:meshcore_sar_app/services/voice_player_service.dart'; import 'package:meshcore_sar_app/widgets/messages/message_bubble.dart'; @@ -28,6 +30,7 @@ void main() { setUp(() { SharedPreferences.setMockInitialValues({}); + LocationTrackingService().currentPosition = null; launchedUrls.clear(); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler( @@ -405,11 +408,60 @@ void main() { expect(find.byType(flutter_map.FlutterMap), findsOneWidget); expect(find.text('46.056900, 14.505800'), findsOneWidget); - expect(find.text('advert'), findsOneWidget); + expect(find.text('shared advert'), findsOneWidget); } finally { await _disposeHarness(tester, harness); } }); + + testWidgets( + 'technical details show a location map for sent channel messages using live position fallback', + (tester) async { + final harness = await _TestHarness.create(); + try { + LocationTrackingService().currentPosition = Position( + latitude: 46.0569, + longitude: 14.5058, + timestamp: DateTime.fromMillisecondsSinceEpoch(1700000003000), + accuracy: 6.0, + altitude: 300.0, + altitudeAccuracy: 6.0, + heading: 0.0, + headingAccuracy: 0.0, + speed: 0.0, + speedAccuracy: 0.0, + ); + + final message = Message( + id: 'channel-live-fallback', + messageType: MessageType.channel, + senderPublicKeyPrefix: _prefix(81), + channelIdx: 0, + pathLen: 0, + textType: MessageTextType.plain, + senderTimestamp: 1700000000, + text: 'Channel fallback', + receivedAt: DateTime.fromMillisecondsSinceEpoch(1700000000500), + deliveryStatus: MessageDeliveryStatus.sent, + ); + + await tester.pumpWidget(_buildApp(harness, message)); + await tester.pumpAndSettle(); + + await tester.longPress(find.text('Channel fallback')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Technical details')); + await tester.pumpAndSettle(); + + expect(find.byType(flutter_map.FlutterMap), findsOneWidget); + expect(find.text('46.056900, 14.505800'), findsOneWidget); + expect(find.text('shared location'), findsOneWidget); + } finally { + await _disposeHarness(tester, harness); + } + }, + ); } Widget _buildApp(_TestHarness harness, Message message) {