fix: Clarify shared location labels and onboarding copy #123

This commit is contained in:
Janez T
2026-04-05 19:54:48 +02:00
parent 14a41b210e
commit 573921e002
4 changed files with 237 additions and 144 deletions

View File

@@ -3267,7 +3267,7 @@ abstract class AppLocalizations {
/// Updated onboarding overview feature 2 /// Updated onboarding overview feature 2
/// ///
/// In en, this message translates to: /// 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; String get wizardOverviewFeature2;
/// Updated onboarding overview feature 3 /// Updated onboarding overview feature 3
@@ -3459,7 +3459,7 @@ abstract class AppLocalizations {
/// Updated onboarding map feature 1 /// Updated onboarding map feature 1
/// ///
/// In en, this message translates to: /// 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; String get wizardMapOpsFeature1;
/// Updated onboarding map feature 2 /// Updated onboarding map feature 2

View File

@@ -15,10 +15,14 @@ class MessageContactLocation {
String get technicalSourceLabel { String get technicalSourceLabel {
switch (source) { switch (source) {
case 'shared':
return 'shared location';
case 'gps':
return 'shared location';
case 'telemetry': case 'telemetry':
return 'telemetry'; return 'live telemetry';
case 'advert': case 'advert':
return 'advert'; return 'shared advert';
default: default:
return source; return source;
} }

View File

@@ -22,7 +22,9 @@ import '../../providers/image_provider.dart' as ip;
import '../../providers/app_provider.dart'; import '../../providers/app_provider.dart';
import '../drawing_minimap_preview.dart'; import '../drawing_minimap_preview.dart';
import '../../models/ble_packet_log.dart'; import '../../models/ble_packet_log.dart';
import '../../models/message_contact_location.dart';
import '../../services/sar_template_service.dart'; import '../../services/sar_template_service.dart';
import '../../services/location_tracking_service.dart';
import '../../utils/toast_logger.dart'; import '../../utils/toast_logger.dart';
import '../../utils/sar_message_parser.dart'; import '../../utils/sar_message_parser.dart';
import '../../utils/key_comparison.dart'; import '../../utils/key_comparison.dart';
@@ -421,6 +423,8 @@ class _MessageBubbleState extends State<MessageBubble> {
} }
void _showMessageOptions(BuildContext context) { void _showMessageOptions(BuildContext context) {
final parentContext = context;
final l10n = AppLocalizations.of(context)!;
final connectionProvider = context.read<ConnectionProvider>(); final connectionProvider = context.read<ConnectionProvider>();
final selfPublicKey = connectionProvider.deviceInfo.publicKey; final selfPublicKey = connectionProvider.deviceInfo.publicKey;
final isOwnMessage = final isOwnMessage =
@@ -433,7 +437,7 @@ class _MessageBubbleState extends State<MessageBubble> {
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)), borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
), ),
builder: (context) => Padding( builder: (sheetContext) => Padding(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -441,29 +445,29 @@ class _MessageBubbleState extends State<MessageBubble> {
if (!isOwnMessage && widget.onReply != null) if (!isOwnMessage && widget.onReply != null)
ListTile( ListTile(
leading: Icon(Icons.reply), leading: Icon(Icons.reply),
title: Text(AppLocalizations.of(context)!.reply), title: Text(l10n.reply),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(sheetContext);
widget.onReply?.call(); widget.onReply?.call();
}, },
), ),
// Copy text option // Copy text option
ListTile( ListTile(
leading: Icon(Icons.copy), leading: Icon(Icons.copy),
title: Text(AppLocalizations.of(context)!.copyText), title: Text(l10n.copyText),
onTap: () { onTap: () {
Clipboard.setData(ClipboardData(text: widget.message.text)); Clipboard.setData(ClipboardData(text: widget.message.text));
Navigator.pop(context); Navigator.pop(sheetContext);
ToastLogger.success( ToastLogger.success(
context, parentContext,
AppLocalizations.of(context)!.textCopiedToClipboard, l10n.textCopiedToClipboard,
); );
}, },
), ),
// Save as Template option (only for SAR markers without existing template) // Save as Template option (only for SAR markers without existing template)
if (widget.message.isSarMarker) if (widget.message.isSarMarker)
Builder( Builder(
builder: (context) { builder: (_) {
// Extract emoji from SAR message // Extract emoji from SAR message
final sarInfo = SarMessageParser.parse(widget.message.text); final sarInfo = SarMessageParser.parse(widget.message.text);
if (sarInfo == null || sarInfo.emoji.isEmpty) { if (sarInfo == null || sarInfo.emoji.isEmpty) {
@@ -482,10 +486,10 @@ class _MessageBubbleState extends State<MessageBubble> {
return ListTile( return ListTile(
leading: Icon(Icons.bookmark_add), leading: Icon(Icons.bookmark_add),
title: Text(AppLocalizations.of(context)!.saveAsTemplate), title: Text(l10n.saveAsTemplate),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(sheetContext);
_saveAsTemplate(context); _saveAsTemplate(parentContext);
}, },
); );
}, },
@@ -495,49 +499,49 @@ class _MessageBubbleState extends State<MessageBubble> {
widget.message.sarGpsCoordinates != null) widget.message.sarGpsCoordinates != null)
ListTile( ListTile(
leading: Icon(Icons.share_location), leading: Icon(Icons.share_location),
title: Text(AppLocalizations.of(context)!.shareLocation), title: Text(l10n.shareLocation),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(sheetContext);
_shareLocation(context); _shareLocation(parentContext);
}, },
), ),
// Navigate to drawing option (only for drawing messages) // Navigate to drawing option (only for drawing messages)
if (widget.message.isDrawing && widget.message.drawingId != null) if (widget.message.isDrawing && widget.message.drawingId != null)
ListTile( ListTile(
leading: Icon(Icons.map), leading: Icon(Icons.map),
title: Text(AppLocalizations.of(context)!.navigateToDrawing), title: Text(l10n.navigateToDrawing),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(sheetContext);
_navigateToDrawing(context); _navigateToDrawing(parentContext);
}, },
), ),
// Copy coordinates option (only for drawing messages) // Copy coordinates option (only for drawing messages)
if (widget.message.isDrawing && widget.message.drawingId != null) if (widget.message.isDrawing && widget.message.drawingId != null)
ListTile( ListTile(
leading: Icon(Icons.copy), leading: Icon(Icons.copy),
title: Text(AppLocalizations.of(context)!.copyCoordinates), title: Text(l10n.copyCoordinates),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(sheetContext);
_copyDrawingCoordinates(context); _copyDrawingCoordinates(parentContext);
}, },
), ),
// Hide from map option (only for drawing messages) // Hide from map option (only for drawing messages)
if (widget.message.isDrawing && widget.message.drawingId != null) if (widget.message.isDrawing && widget.message.drawingId != null)
ListTile( ListTile(
leading: Icon(Icons.visibility_off), leading: Icon(Icons.visibility_off),
title: Text(AppLocalizations.of(context)!.hideFromMap), title: Text(l10n.hideFromMap),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(sheetContext);
_hideDrawingFromMap(context); _hideDrawingFromMap(parentContext);
}, },
), ),
// Technical details option // Technical details option
ListTile( ListTile(
leading: Icon(Icons.data_object), leading: Icon(Icons.data_object),
title: Text(AppLocalizations.of(context)!.technicalDetails), title: Text(l10n.technicalDetails),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(sheetContext);
_showTechnicalDetails(context); _showTechnicalDetails(parentContext);
}, },
), ),
if (!isOwnMessage && if (!isOwnMessage &&
@@ -545,22 +549,22 @@ class _MessageBubbleState extends State<MessageBubble> {
widget.message.pathLen < 255) widget.message.pathLen < 255)
ListTile( ListTile(
leading: Icon(Icons.route), leading: Icon(Icons.route),
title: Text(AppLocalizations.of(context)!.trace), title: Text(l10n.trace),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(sheetContext);
_showTraceSheet(context); _showTraceSheet(parentContext);
}, },
), ),
// Delete message option // Delete message option
ListTile( ListTile(
leading: Icon(Icons.delete, color: Colors.red), leading: Icon(Icons.delete, color: Colors.red),
title: Text( title: Text(
AppLocalizations.of(context)!.delete, l10n.delete,
style: const TextStyle(color: Colors.red), style: const TextStyle(color: Colors.red),
), ),
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(sheetContext);
_showDeleteConfirmation(context); _showDeleteConfirmation(parentContext);
}, },
), ),
], ],
@@ -582,6 +586,7 @@ class _MessageBubbleState extends State<MessageBubble> {
} }
void _showTechnicalDetails(BuildContext context) { void _showTechnicalDetails(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final connectionProvider = context.read<ConnectionProvider>(); final connectionProvider = context.read<ConnectionProvider>();
final radioBw = connectionProvider.deviceInfo.radioBw; final radioBw = connectionProvider.deviceInfo.radioBw;
final radioSf = connectionProvider.deviceInfo.radioSf; final radioSf = connectionProvider.deviceInfo.radioSf;
@@ -596,6 +601,7 @@ class _MessageBubbleState extends State<MessageBubble> {
widget.message.isFromSelf(selfPublicKey); widget.message.isFromSelf(selfPublicKey);
String? senderName; String? senderName;
Contact? senderContact;
if (widget.message.senderPublicKeyPrefix != null) { if (widget.message.senderPublicKeyPrefix != null) {
final senderKeyHex = widget.message.senderPublicKeyPrefix! final senderKeyHex = widget.message.senderPublicKeyPrefix!
.sublist( .sublist(
@@ -606,7 +612,7 @@ class _MessageBubbleState extends State<MessageBubble> {
) )
.map((b) => b.toRadixString(16).padLeft(2, '0')) .map((b) => b.toRadixString(16).padLeft(2, '0'))
.join(''); .join('');
final senderContact = contactsProvider.contacts senderContact = contactsProvider.contacts
.where((c) => c.publicKeyHex.startsWith(senderKeyHex)) .where((c) => c.publicKeyHex.startsWith(senderKeyHex))
.firstOrNull; .firstOrNull;
senderName = senderContact?.advName; senderName = senderContact?.advName;
@@ -629,7 +635,7 @@ class _MessageBubbleState extends State<MessageBubble> {
recipientName = recipientContact?.advName; recipientName = recipientContact?.advName;
} }
final messageLocationSnapshot = messagesProvider.getMessageContactLocation( final storedLocationSnapshot = messagesProvider.getMessageContactLocation(
widget.message.id, widget.message.id,
); );
final receptionDetails = messagesProvider.getMessageReceptionDetails( final receptionDetails = messagesProvider.getMessageReceptionDetails(
@@ -710,6 +716,12 @@ class _MessageBubbleState extends State<MessageBubble> {
final routeMetadata = context final routeMetadata = context
.read<MessagesProvider>() .read<MessagesProvider>()
.getMessageRouteMetadata(widget.message.id); .getMessageRouteMetadata(widget.message.id);
final messageLocationSnapshot = _resolveDetailsLocationSnapshot(
storedSnapshot: storedLocationSnapshot,
senderContact: senderContact,
connectionProvider: connectionProvider,
isOwnMessage: isOwnMessage,
);
final rawLines = <String>[ final rawLines = <String>[
'Message ID: ${widget.message.id}', 'Message ID: ${widget.message.id}',
@@ -834,12 +846,9 @@ class _MessageBubbleState extends State<MessageBubble> {
} }
} }
void copyField(String value) { void copyField(BuildContext feedbackContext, String value) {
Clipboard.setData(ClipboardData(text: value)); Clipboard.setData(ClipboardData(text: value));
ToastLogger.success( ToastLogger.success(feedbackContext, l10n.textCopiedToClipboard);
context,
AppLocalizations.of(context)!.textCopiedToClipboard,
);
} }
showModalBottomSheet( showModalBottomSheet(
@@ -861,14 +870,14 @@ class _MessageBubbleState extends State<MessageBubble> {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
AppLocalizations.of(context)!.messageTechnicalDetails, l10n.messageTechnicalDetails,
style: Theme.of(sheetContext).textTheme.titleLarge, style: Theme.of(sheetContext).textTheme.titleLarge,
), ),
), ),
IconButton( IconButton(
onPressed: () => Navigator.pop(sheetContext), onPressed: () => Navigator.pop(sheetContext),
icon: Icon(Icons.close), icon: Icon(Icons.close),
tooltip: AppLocalizations.of(context)!.close, tooltip: l10n.close,
), ),
], ],
), ),
@@ -913,7 +922,7 @@ class _MessageBubbleState extends State<MessageBubble> {
_techSection( _techSection(
sheetContext, sheetContext,
icon: Icons.location_on, icon: Icons.location_on,
title: AppLocalizations.of(context)!.location, title: l10n.location,
child: Column( child: Column(
children: [ children: [
_buildLocationPreviewMap( _buildLocationPreviewMap(
@@ -923,29 +932,29 @@ class _MessageBubbleState extends State<MessageBubble> {
const SizedBox(height: 12), const SizedBox(height: 12),
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.coordinates,
context,
)!.coordinates,
value: messageLocationSnapshot value: messageLocationSnapshot
.formattedCoordinates, .formattedCoordinates,
onCopy: () => copyField( onCopy: () => copyField(
sheetContext,
messageLocationSnapshot.formattedCoordinates, messageLocationSnapshot.formattedCoordinates,
), ),
), ),
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.source, label: l10n.source,
value: messageLocationSnapshot value: messageLocationSnapshot
.technicalSourceLabel, .technicalSourceLabel,
), ),
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.captured, label: l10n.captured,
value: _formatRfc3339( value: _formatRfc3339(
messageLocationSnapshot.sourceTimestamp ?? messageLocationSnapshot.sourceTimestamp ??
messageLocationSnapshot.capturedAt, messageLocationSnapshot.capturedAt,
), ),
onCopy: () => copyField( onCopy: () => copyField(
sheetContext,
_formatRfc3339( _formatRfc3339(
messageLocationSnapshot.sourceTimestamp ?? messageLocationSnapshot.sourceTimestamp ??
messageLocationSnapshot.capturedAt, messageLocationSnapshot.capturedAt,
@@ -963,13 +972,13 @@ class _MessageBubbleState extends State<MessageBubble> {
_techSection( _techSection(
sheetContext, sheetContext,
icon: Icons.network_check, icon: Icons.network_check,
title: AppLocalizations.of(context)!.linkQuality, title: l10n.linkQuality,
child: Column( child: Column(
children: [ children: [
if (rssiDbm != null) if (rssiDbm != null)
_signalRow( _signalRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.rssi, label: l10n.rssi,
valueLabel: '$rssiDbm dBm', valueLabel: '$rssiDbm dBm',
normalized: normalized:
((rssiDbm.toDouble() + 120.0) / 70.0) ((rssiDbm.toDouble() + 120.0) / 70.0)
@@ -984,7 +993,7 @@ class _MessageBubbleState extends State<MessageBubble> {
SizedBox(height: 8), SizedBox(height: 8),
_signalRow( _signalRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.snr, label: l10n.snr,
valueLabel: '${snrDb.toStringAsFixed(1)} dB', valueLabel: '${snrDb.toStringAsFixed(1)} dB',
normalized: ((snrDb + 20.0) / 40.0).clamp( normalized: ((snrDb + 20.0) / 40.0).clamp(
0.0, 0.0,
@@ -1005,39 +1014,34 @@ class _MessageBubbleState extends State<MessageBubble> {
_techSection( _techSection(
sheetContext, sheetContext,
icon: Icons.tune, icon: Icons.tune,
title: AppLocalizations.of(context)!.delivery, title: l10n.delivery,
child: Column( child: Column(
children: [ children: [
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.status, label: l10n.status,
value: widget.message.deliveryStatus.name, value: widget.message.deliveryStatus.name,
), ),
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.receivedRfc3339,
context,
)!.receivedRfc3339,
value: _formatRfc3339(widget.message.receivedAt), value: _formatRfc3339(widget.message.receivedAt),
onCopy: () => copyField( onCopy: () => copyField(
sheetContext,
_formatRfc3339(widget.message.receivedAt), _formatRfc3339(widget.message.receivedAt),
), ),
), ),
if (widget.message.expectedAckTag != null) if (widget.message.expectedAckTag != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.expectedAckTag,
context,
)!.expectedAckTag,
value: widget.message.expectedAckTag! value: widget.message.expectedAckTag!
.toString(), .toString(),
), ),
if (receptionDetails?.senderToReceiptMs != null) if (receptionDetails?.senderToReceiptMs != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.senderToReceipt,
context,
)!.senderToReceipt,
value: _formatDurationMs( value: _formatDurationMs(
receptionDetails!.senderToReceiptMs!, receptionDetails!.senderToReceiptMs!,
), ),
@@ -1045,9 +1049,7 @@ class _MessageBubbleState extends State<MessageBubble> {
if (receptionDetails?.estimatedTransmitMs != null) if (receptionDetails?.estimatedTransmitMs != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.estimatedTx,
context,
)!.estimatedTx,
value: _formatDurationMs( value: _formatDurationMs(
receptionDetails!.estimatedTransmitMs!, receptionDetails!.estimatedTransmitMs!,
), ),
@@ -1055,9 +1057,7 @@ class _MessageBubbleState extends State<MessageBubble> {
if (receptionDetails?.postTransmitDelayMs != null) if (receptionDetails?.postTransmitDelayMs != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.posttxDelay,
context,
)!.posttxDelay,
value: _formatDurationMs( value: _formatDurationMs(
receptionDetails!.postTransmitDelayMs!, receptionDetails!.postTransmitDelayMs!,
), ),
@@ -1065,87 +1065,80 @@ class _MessageBubbleState extends State<MessageBubble> {
if (widget.receivedCopies > 1) if (widget.receivedCopies > 1)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.receivedCopies,
context,
)!.receivedCopies,
value: '${widget.receivedCopies}', value: '${widget.receivedCopies}',
), ),
if (widget.message.suggestedTimeoutMs != null) if (widget.message.suggestedTimeoutMs != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.ackTimeout, label: l10n.ackTimeout,
value: value:
'${widget.message.suggestedTimeoutMs} ms', '${widget.message.suggestedTimeoutMs} ms',
), ),
if (retryCause != null) if (retryCause != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.retryCause, label: l10n.retryCause,
value: retryCause, value: retryCause,
), ),
if (retryMode != null) if (retryMode != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.retryMode, label: l10n.retryMode,
value: retryMode, value: retryMode,
), ),
if (widget.message.roundTripTimeMs != null) if (widget.message.roundTripTimeMs != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.roundTrip, label: l10n.roundTrip,
value: '${widget.message.roundTripTimeMs} ms', value: '${widget.message.roundTripTimeMs} ms',
), ),
if (widget.message.retryAttempt > 0) if (widget.message.retryAttempt > 0)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.retryAttempt,
context,
)!.retryAttempt,
value: '${widget.message.retryAttempt}/4', value: '${widget.message.retryAttempt}/4',
), ),
if (widget.message.lastRetryAt != null) if (widget.message.lastRetryAt != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.lastRetry, label: l10n.lastRetry,
value: _formatRfc3339( value: _formatRfc3339(
widget.message.lastRetryAt!, widget.message.lastRetryAt!,
), ),
onCopy: () => copyField( onCopy: () => copyField(
sheetContext,
_formatRfc3339(widget.message.lastRetryAt!), _formatRfc3339(widget.message.lastRetryAt!),
), ),
), ),
if (widget.message.usedFloodFallback) if (widget.message.usedFloodFallback)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.floodFallback,
context, value: l10n.yes,
)!.floodFallback,
value: AppLocalizations.of(context)!.yes,
), ),
if (routeMetadata?.canonicalPath if (routeMetadata?.canonicalPath
case final routePath?) case final routePath?)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.selectedPath,
context,
)!.selectedPath,
value: routePath, value: routePath,
onCopy: () => copyField(routePath), onCopy: () =>
copyField(sheetContext, routePath),
), ),
if (retryResult != null) if (retryResult != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.retryResult,
context,
)!.retryResult,
value: retryResult, value: retryResult,
), ),
if (packetPathHex != null) if (packetPathHex != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.pathBytes, label: l10n.pathBytes,
value: packetPathHex, value: packetPathHex,
onCopy: () => copyField(packetPathHex), onCopy: () =>
copyField(sheetContext, packetPathHex),
), ),
], ],
), ),
@@ -1154,18 +1147,19 @@ class _MessageBubbleState extends State<MessageBubble> {
_techSection( _techSection(
sheetContext, sheetContext,
icon: Icons.badge, icon: Icons.badge,
title: AppLocalizations.of(context)!.identity, title: l10n.identity,
child: Column( child: Column(
children: [ children: [
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.messageId, label: l10n.messageId,
value: widget.message.id, value: widget.message.id,
onCopy: () => copyField(widget.message.id), onCopy: () =>
copyField(sheetContext, widget.message.id),
), ),
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.sender, label: l10n.sender,
value: value:
senderName ?? senderName ??
widget.message.senderName ?? widget.message.senderName ??
@@ -1174,24 +1168,26 @@ class _MessageBubbleState extends State<MessageBubble> {
if (senderPrefixHex != null) if (senderPrefixHex != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.senderKey, label: l10n.senderKey,
value: senderPrefixHex, value: senderPrefixHex,
onCopy: () => copyField(senderPrefixHex), onCopy: () =>
copyField(sheetContext, senderPrefixHex),
), ),
if (recipientName != null) if (recipientName != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.recipient, label: l10n.recipient,
value: recipientName, value: recipientName,
), ),
if (recipientPrefixHex != null) if (recipientPrefixHex != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.recipientKey,
context,
)!.recipientKey,
value: recipientPrefixHex, value: recipientPrefixHex,
onCopy: () => copyField(recipientPrefixHex), onCopy: () => copyField(
sheetContext,
recipientPrefixHex,
),
), ),
], ],
), ),
@@ -1201,53 +1197,47 @@ class _MessageBubbleState extends State<MessageBubble> {
_techSection( _techSection(
sheetContext, sheetContext,
icon: Icons.graphic_eq, icon: Icons.graphic_eq,
title: AppLocalizations.of(context)!.voice, title: l10n.voice,
child: Column( child: Column(
children: [ children: [
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.voiceId, label: l10n.voiceId,
value: widget.message.voiceId ?? '-', value: widget.message.voiceId ?? '-',
), ),
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.envelope, label: l10n.envelope,
value: envelope != null value: envelope != null
? 'VE3 compact' ? 'VE3 compact'
: AppLocalizations.of(context)!.unknown, : l10n.unknown,
), ),
if (voiceSession != null) if (voiceSession != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.sessionProgress,
context,
)!.sessionProgress,
value: value:
'${voiceSession.receivedCount}/${voiceSession.total} segments', '${voiceSession.receivedCount}/${voiceSession.total} segments',
), ),
if (voiceSession != null) if (voiceSession != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.complete, label: l10n.complete,
value: voiceSession.isComplete value: voiceSession.isComplete
? AppLocalizations.of(context)!.yes ? l10n.yes
: AppLocalizations.of(context)!.no, : l10n.no,
), ),
if (transferDetails != null) if (transferDetails != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.transfers,
context,
)!.transfers,
value: '${transferDetails.totalTransfers}', value: '${transferDetails.totalTransfers}',
), ),
if (transferDetails != null && if (transferDetails != null &&
transferDetails.downloaders.isNotEmpty) transferDetails.downloaders.isNotEmpty)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.downloadedBy,
context,
)!.downloadedBy,
value: _formatDownloaderSummary( value: _formatDownloaderSummary(
transferDetails, transferDetails,
), ),
@@ -1255,9 +1245,7 @@ class _MessageBubbleState extends State<MessageBubble> {
if (voiceTxEstimate > Duration.zero) if (voiceTxEstimate > Duration.zero)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.estimatedTx,
context,
)!.estimatedTx,
value: voiceTxEstimate.inSeconds < 60 value: voiceTxEstimate.inSeconds < 60
? '~${voiceTxEstimate.inSeconds}s' ? '~${voiceTxEstimate.inSeconds}s'
: '~${voiceTxEstimate.inMinutes}m ${voiceTxEstimate.inSeconds % 60}s', : '~${voiceTxEstimate.inMinutes}m ${voiceTxEstimate.inSeconds % 60}s',
@@ -1271,28 +1259,28 @@ class _MessageBubbleState extends State<MessageBubble> {
_techSection( _techSection(
sheetContext, sheetContext,
icon: Icons.image_outlined, icon: Icons.image_outlined,
title: AppLocalizations.of(context)!.image, title: l10n.image,
child: Column( child: Column(
children: [ children: [
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.envelope, label: l10n.envelope,
value: 'IE1', value: 'IE1',
), ),
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.format, label: l10n.format,
value: imageEnvelope.format.label, value: imageEnvelope.format.label,
), ),
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.dimensions, label: l10n.dimensions,
value: value:
'${imageEnvelope.width}×${imageEnvelope.height}', '${imageEnvelope.width}×${imageEnvelope.height}',
), ),
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.segments, label: l10n.segments,
value: imageSession != null value: imageSession != null
? '${imageSession.receivedCount}/${imageSession.total}' ? '${imageSession.receivedCount}/${imageSession.total}'
: '${imageEnvelope.total}', : '${imageEnvelope.total}',
@@ -1300,26 +1288,22 @@ class _MessageBubbleState extends State<MessageBubble> {
if (imageSession != null) if (imageSession != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of(context)!.complete, label: l10n.complete,
value: imageSession.isComplete value: imageSession.isComplete
? AppLocalizations.of(context)!.yes ? l10n.yes
: AppLocalizations.of(context)!.no, : l10n.no,
), ),
if (transferDetails != null) if (transferDetails != null)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.transfers,
context,
)!.transfers,
value: '${transferDetails.totalTransfers}', value: '${transferDetails.totalTransfers}',
), ),
if (transferDetails != null && if (transferDetails != null &&
transferDetails.downloaders.isNotEmpty) transferDetails.downloaders.isNotEmpty)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.downloadedBy,
context,
)!.downloadedBy,
value: _formatDownloaderSummary( value: _formatDownloaderSummary(
transferDetails, transferDetails,
), ),
@@ -1327,9 +1311,7 @@ class _MessageBubbleState extends State<MessageBubble> {
if (imageTxEstimate > Duration.zero) if (imageTxEstimate > Duration.zero)
_detailRow( _detailRow(
sheetContext, sheetContext,
label: AppLocalizations.of( label: l10n.estimatedTx,
context,
)!.estimatedTx,
value: imageTxEstimate.inSeconds < 60 value: imageTxEstimate.inSeconds < 60
? '~${imageTxEstimate.inSeconds}s' ? '~${imageTxEstimate.inSeconds}s'
: '~${imageTxEstimate.inMinutes}m ${imageTxEstimate.inSeconds % 60}s', : '~${imageTxEstimate.inMinutes}m ${imageTxEstimate.inSeconds % 60}s',
@@ -1344,7 +1326,7 @@ class _MessageBubbleState extends State<MessageBubble> {
dense: true, dense: true,
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
title: Text( title: Text(
AppLocalizations.of(context)!.rawDump, l10n.rawDump,
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -1429,6 +1411,61 @@ class _MessageBubbleState extends State<MessageBubble> {
); );
} }
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( Widget _techSection(
BuildContext context, { BuildContext context, {
required IconData icon, required IconData icon,

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart' as flutter_map; import 'package:flutter_map/flutter_map.dart' as flutter_map;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.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/l10n/app_localizations.dart';
import 'package:meshcore_sar_app/models/message.dart'; import 'package:meshcore_sar_app/models/message.dart';
import 'package:meshcore_sar_app/models/message_contact_location.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/image_provider.dart' as ip;
import 'package:meshcore_sar_app/providers/messages_provider.dart'; import 'package:meshcore_sar_app/providers/messages_provider.dart';
import 'package:meshcore_sar_app/providers/voice_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_codec_service.dart';
import 'package:meshcore_sar_app/services/voice_player_service.dart'; import 'package:meshcore_sar_app/services/voice_player_service.dart';
import 'package:meshcore_sar_app/widgets/messages/message_bubble.dart'; import 'package:meshcore_sar_app/widgets/messages/message_bubble.dart';
@@ -28,6 +30,7 @@ void main() {
setUp(() { setUp(() {
SharedPreferences.setMockInitialValues({}); SharedPreferences.setMockInitialValues({});
LocationTrackingService().currentPosition = null;
launchedUrls.clear(); launchedUrls.clear();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler( .setMockMethodCallHandler(
@@ -405,11 +408,60 @@ void main() {
expect(find.byType(flutter_map.FlutterMap), findsOneWidget); expect(find.byType(flutter_map.FlutterMap), findsOneWidget);
expect(find.text('46.056900, 14.505800'), findsOneWidget); expect(find.text('46.056900, 14.505800'), findsOneWidget);
expect(find.text('advert'), findsOneWidget); expect(find.text('shared advert'), findsOneWidget);
} finally { } finally {
await _disposeHarness(tester, harness); 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) { Widget _buildApp(_TestHarness harness, Message message) {