mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Add message location details
This commit is contained in:
@@ -6,6 +6,7 @@ import 'package:geolocator/geolocator.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import '../../models/contact.dart';
|
||||
import '../../models/room_login_state.dart';
|
||||
import '../../providers/app_provider.dart';
|
||||
import '../../providers/connection_provider.dart';
|
||||
import '../../providers/contacts_provider.dart';
|
||||
import '../../providers/map_provider.dart';
|
||||
@@ -66,6 +67,28 @@ class ContactTile extends StatelessWidget {
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
Widget _buildChannelLocationSharingMarker(
|
||||
BuildContext context,
|
||||
ChannelLocationSharingMode mode,
|
||||
) {
|
||||
final isHardware = mode == ChannelLocationSharingMode.hardware;
|
||||
const accent = Color(0xFF16A34A);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
color: accent,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
child: Icon(
|
||||
isHardware ? Icons.public_rounded : Icons.smartphone_rounded,
|
||||
size: 11,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isChannel = contact.type == ContactType.channel;
|
||||
@@ -90,6 +113,7 @@ class ContactTile extends StatelessWidget {
|
||||
// Get room login state if this is a room
|
||||
final connectionProvider = context.watch<ConnectionProvider>();
|
||||
final messagesProvider = context.watch<MessagesProvider>();
|
||||
final appProvider = context.watch<AppProvider>();
|
||||
final isPingInProgress = connectionProvider.isPingInProgress(
|
||||
contact.publicKey,
|
||||
);
|
||||
@@ -134,6 +158,10 @@ class ContactTile extends StatelessWidget {
|
||||
final lastActivityAt = isRoomOrChannel
|
||||
? messagesProvider.getLastActivityForDestination(contact)
|
||||
: null;
|
||||
final channelLocationSharingMode =
|
||||
isChannel && !contact.isPublicChannel && contact.publicKey.length > 1
|
||||
? appProvider.channelLocationSharingModeForChannel(contact.publicKey[1])
|
||||
: null;
|
||||
final timeAgoText = _getLocalizedRelativeTime(
|
||||
context,
|
||||
lastActivityAt ?? contact.lastSeenTime,
|
||||
@@ -239,6 +267,15 @@ class ContactTile extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isChannel && channelLocationSharingMode != null)
|
||||
Positioned(
|
||||
bottom: -2,
|
||||
right: -2,
|
||||
child: _buildChannelLocationSharingMarker(
|
||||
context,
|
||||
channelLocationSharingMode,
|
||||
),
|
||||
),
|
||||
if (contact.type == ContactType.room &&
|
||||
roomLoginState != null)
|
||||
Positioned(
|
||||
@@ -383,6 +420,68 @@ class ContactTile extends StatelessWidget {
|
||||
_showContactActionSheet(context, contact);
|
||||
}
|
||||
|
||||
String _locationSharingErrorMessage(Object error) {
|
||||
final message = error.toString();
|
||||
if (message.startsWith('Bad state: ')) {
|
||||
return message.substring('Bad state: '.length);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
Future<void> _setChannelLocationSharing(
|
||||
BuildContext context,
|
||||
Contact contact,
|
||||
bool enabled,
|
||||
) async {
|
||||
final channelIdx = contact.publicKey.length > 1 ? contact.publicKey[1] : 0;
|
||||
try {
|
||||
final result = await context.read<AppProvider>().setChannelLocationSharingEnabled(
|
||||
channelIdx,
|
||||
enabled,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
ToastLogger.success(context, result.message);
|
||||
} catch (error) {
|
||||
if (!context.mounted) return;
|
||||
ToastLogger.error(context, _locationSharingErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleChannelLocationSharingAction(
|
||||
BuildContext context,
|
||||
Contact contact,
|
||||
) async {
|
||||
if (contact.isPublicChannel) {
|
||||
if (!context.mounted) return;
|
||||
ToastLogger.error(
|
||||
context,
|
||||
'Select a private channel to share your location.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final channelIdx = contact.publicKey.length > 1 ? contact.publicKey[1] : 0;
|
||||
if (channelIdx <= 0) {
|
||||
if (!context.mounted) return;
|
||||
ToastLogger.error(
|
||||
context,
|
||||
'Select a private channel to share your location.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final sharingState = await context
|
||||
.read<AppProvider>()
|
||||
.getChannelLocationSharingState(channelIdx);
|
||||
if (!context.mounted) return;
|
||||
await _setChannelLocationSharing(context, contact, !sharingState.isSharing);
|
||||
} catch (error) {
|
||||
if (!context.mounted) return;
|
||||
ToastLogger.error(context, _locationSharingErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
void _showContactActionSheet(BuildContext context, Contact contact) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final canToggleFavourite = !contact.isChannel;
|
||||
@@ -402,179 +501,220 @@ class ContactTile extends StatelessWidget {
|
||||
final canPreviewSensor = contact.isSensor;
|
||||
final sensorsProvider = context.read<SensorsProvider>();
|
||||
final isInSensors = sensorsProvider.isWatched(contact.publicKeyHex);
|
||||
|
||||
final primaryActions = <_ContactSheetAction>[
|
||||
if (canMessage)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.message_outlined,
|
||||
label: l10n.messages,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
await _openMessagesForContact(context, contact);
|
||||
},
|
||||
),
|
||||
if (!contact.isChannel)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.share_outlined,
|
||||
label: l10n.share,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
final url = await connectionProvider.exportContactUrl(
|
||||
contact.publicKey,
|
||||
);
|
||||
if (url != null && context.mounted) {
|
||||
await Clipboard.setData(ClipboardData(text: url));
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.contactLinkCopiedToClipboard)),
|
||||
);
|
||||
}
|
||||
} else if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.failedToExportContact)),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (canSetPath)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.alt_route,
|
||||
label: l10n.contactSetPath,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
await _showSetRouteDialog(context, contact);
|
||||
},
|
||||
),
|
||||
if (!contact.isPublicChannel)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.delete_outline_rounded,
|
||||
label: l10n.delete,
|
||||
destructive: true,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
if (!context.mounted) return;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!context.mounted) return;
|
||||
if (contact.isChannel) {
|
||||
_showDeleteChannelDialog(context, contact);
|
||||
} else {
|
||||
_showDeleteConfirmation(context, contact);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
final secondaryActions = <_ContactSheetAction>[
|
||||
if (contact.displayLocation != null)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.map_outlined,
|
||||
label: l10n.viewOnMap,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
_showContactOnMap(context, contact);
|
||||
},
|
||||
),
|
||||
if (contact.type == ContactType.room && !contact.isPublicChannel)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.login,
|
||||
label:
|
||||
context
|
||||
.read<ConnectionProvider>()
|
||||
.getRoomLoginState(contact.publicKeyPrefix)
|
||||
?.isLoggedIn ==
|
||||
true
|
||||
? l10n.reLoginToRoom
|
||||
: l10n.loginToRoom,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
_showRoomLoginDialog(context, contact);
|
||||
},
|
||||
),
|
||||
if (canPreviewSensor)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.visibility_outlined,
|
||||
label: l10n.preview,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
if (!context.mounted) return;
|
||||
await _showSensorPreviewView(context, contact);
|
||||
},
|
||||
),
|
||||
if (canAddToSensors)
|
||||
_ContactSheetAction(
|
||||
icon: isInSensors ? Icons.sensors : Icons.sensors_outlined,
|
||||
label: isInSensors ? l10n.contactInSensors : l10n.contactAddToSensors,
|
||||
enabled: !isInSensors,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
await _addContactToSensors(context, contact);
|
||||
},
|
||||
),
|
||||
if (!contact.isChannel)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.route,
|
||||
label: l10n.trace,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
_showTraceSheet(context, contact);
|
||||
},
|
||||
),
|
||||
if (contact.type == ContactType.repeater)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.hub_outlined,
|
||||
label: 'View Neighbours',
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
_showNeighbours(context, contact);
|
||||
},
|
||||
),
|
||||
if (contact.type == ContactType.repeater ||
|
||||
contact.type == ContactType.room)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.network_ping,
|
||||
label: 'Ping',
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
_pingRelay(context, contact);
|
||||
},
|
||||
),
|
||||
if (!contact.isPublicChannel)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.edit_outlined,
|
||||
label: l10n.editName,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
_showNameOverrideDialog(context, contact);
|
||||
},
|
||||
),
|
||||
];
|
||||
final channelLocationSharingFuture =
|
||||
contact.type == ContactType.channel &&
|
||||
!contact.isPublicChannel &&
|
||||
contact.publicKey.length > 1
|
||||
? context
|
||||
.read<AppProvider>()
|
||||
.getChannelLocationSharingState(contact.publicKey[1])
|
||||
: null;
|
||||
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (sheetContext) => _ContactActionSheet(
|
||||
contact: contact,
|
||||
primaryActions: primaryActions,
|
||||
secondaryActions: secondaryActions,
|
||||
showFavouriteButton: canToggleFavourite,
|
||||
initialFavourite: contact.isFavourite,
|
||||
onClose: () => Navigator.pop(sheetContext),
|
||||
onToggleFavourite: !canToggleFavourite
|
||||
? null
|
||||
: () async {
|
||||
final toggled = contact.toggleFavourite();
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
await connectionProvider.addOrUpdateContact(toggled);
|
||||
if (context.mounted) {
|
||||
await connectionProvider.getContact(contact.publicKey);
|
||||
}
|
||||
},
|
||||
),
|
||||
builder: (sheetContext) {
|
||||
Widget buildSheet(ChannelLocationSharingState? sharingState) {
|
||||
final primaryActions = <_ContactSheetAction>[
|
||||
if (canMessage)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.message_outlined,
|
||||
label: l10n.messages,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
await _openMessagesForContact(context, contact);
|
||||
},
|
||||
),
|
||||
if (contact.type == ContactType.channel)
|
||||
_ContactSheetAction(
|
||||
icon: sharingState?.isSharing == true
|
||||
? Icons.location_off_rounded
|
||||
: Icons.share_location_rounded,
|
||||
label: sharingState?.isSharing == true
|
||||
? 'Stop sharing my location'
|
||||
: 'Share my location',
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
await _handleChannelLocationSharingAction(context, contact);
|
||||
},
|
||||
),
|
||||
if (!contact.isChannel)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.share_outlined,
|
||||
label: l10n.share,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
final url = await connectionProvider.exportContactUrl(
|
||||
contact.publicKey,
|
||||
);
|
||||
if (url != null && context.mounted) {
|
||||
await Clipboard.setData(ClipboardData(text: url));
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.contactLinkCopiedToClipboard),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.failedToExportContact)),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (canSetPath)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.alt_route,
|
||||
label: l10n.contactSetPath,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
await _showSetRouteDialog(context, contact);
|
||||
},
|
||||
),
|
||||
if (!contact.isPublicChannel)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.delete_outline_rounded,
|
||||
label: l10n.delete,
|
||||
destructive: true,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
if (!context.mounted) return;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!context.mounted) return;
|
||||
if (contact.isChannel) {
|
||||
_showDeleteChannelDialog(context, contact);
|
||||
} else {
|
||||
_showDeleteConfirmation(context, contact);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
final secondaryActions = <_ContactSheetAction>[
|
||||
if (contact.displayLocation != null)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.map_outlined,
|
||||
label: l10n.viewOnMap,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
_showContactOnMap(context, contact);
|
||||
},
|
||||
),
|
||||
if (contact.type == ContactType.room && !contact.isPublicChannel)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.login,
|
||||
label:
|
||||
context
|
||||
.read<ConnectionProvider>()
|
||||
.getRoomLoginState(contact.publicKeyPrefix)
|
||||
?.isLoggedIn ==
|
||||
true
|
||||
? l10n.reLoginToRoom
|
||||
: l10n.loginToRoom,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
_showRoomLoginDialog(context, contact);
|
||||
},
|
||||
),
|
||||
if (canPreviewSensor)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.visibility_outlined,
|
||||
label: l10n.preview,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
if (!context.mounted) return;
|
||||
await _showSensorPreviewView(context, contact);
|
||||
},
|
||||
),
|
||||
if (canAddToSensors)
|
||||
_ContactSheetAction(
|
||||
icon: isInSensors ? Icons.sensors : Icons.sensors_outlined,
|
||||
label: isInSensors
|
||||
? l10n.contactInSensors
|
||||
: l10n.contactAddToSensors,
|
||||
enabled: !isInSensors,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
await _addContactToSensors(context, contact);
|
||||
},
|
||||
),
|
||||
if (!contact.isChannel)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.route,
|
||||
label: l10n.trace,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
_showTraceSheet(context, contact);
|
||||
},
|
||||
),
|
||||
if (contact.type == ContactType.repeater)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.hub_outlined,
|
||||
label: 'View Neighbours',
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
_showNeighbours(context, contact);
|
||||
},
|
||||
),
|
||||
if (contact.type == ContactType.repeater ||
|
||||
contact.type == ContactType.room)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.network_ping,
|
||||
label: 'Ping',
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
_pingRelay(context, contact);
|
||||
},
|
||||
),
|
||||
if (!contact.isPublicChannel)
|
||||
_ContactSheetAction(
|
||||
icon: Icons.edit_outlined,
|
||||
label: l10n.editName,
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
_showNameOverrideDialog(context, contact);
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
return _ContactActionSheet(
|
||||
contact: contact,
|
||||
primaryActions: primaryActions,
|
||||
secondaryActions: secondaryActions,
|
||||
showFavouriteButton: canToggleFavourite,
|
||||
initialFavourite: contact.isFavourite,
|
||||
onClose: () => Navigator.pop(sheetContext),
|
||||
onToggleFavourite: !canToggleFavourite
|
||||
? null
|
||||
: () async {
|
||||
final toggled = contact.toggleFavourite();
|
||||
final connectionProvider =
|
||||
context.read<ConnectionProvider>();
|
||||
await connectionProvider.addOrUpdateContact(toggled);
|
||||
if (context.mounted) {
|
||||
await connectionProvider.getContact(contact.publicKey);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (channelLocationSharingFuture == null) {
|
||||
return buildSheet(null);
|
||||
}
|
||||
|
||||
return FutureBuilder<ChannelLocationSharingState>(
|
||||
future: channelLocationSharingFuture,
|
||||
builder: (context, snapshot) {
|
||||
return buildSheet(snapshot.data);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter_map/flutter_map.dart' as flutter_map;
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -628,7 +629,7 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
recipientName = recipientContact?.advName;
|
||||
}
|
||||
|
||||
final senderLocationSnapshot = messagesProvider.getMessageContactLocation(
|
||||
final messageLocationSnapshot = messagesProvider.getMessageContactLocation(
|
||||
widget.message.id,
|
||||
);
|
||||
final receptionDetails = messagesProvider.getMessageReceptionDetails(
|
||||
@@ -742,9 +743,9 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
'Retry result: ${retryResult ?? '-'}',
|
||||
'Sender key prefix: ${senderPrefixHex ?? '-'}',
|
||||
'Sender name: ${senderName ?? widget.message.senderName ?? '-'}',
|
||||
'Sender location at receipt: ${senderLocationSnapshot?.formattedCoordinates ?? '-'}',
|
||||
'Sender location source: ${senderLocationSnapshot?.technicalSourceLabel ?? '-'}',
|
||||
'Sender location timestamp: ${senderLocationSnapshot?.sourceTimestamp?.toIso8601String() ?? '-'}',
|
||||
'${widget.message.isSentMessage ? "Sent from location" : "Sender location at receipt"}: ${messageLocationSnapshot?.formattedCoordinates ?? '-'}',
|
||||
'${widget.message.isSentMessage ? "Sent from source" : "Sender location source"}: ${messageLocationSnapshot?.technicalSourceLabel ?? '-'}',
|
||||
'${widget.message.isSentMessage ? "Sent from timestamp" : "Sender location timestamp"}: ${messageLocationSnapshot?.sourceTimestamp?.toIso8601String() ?? '-'}',
|
||||
'Recipient key prefix: ${recipientPrefixHex ?? '-'}',
|
||||
'Recipient name: ${recipientName ?? '-'}',
|
||||
'Drawing flag: ${widget.message.isDrawing}',
|
||||
@@ -907,6 +908,54 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (messageLocationSnapshot != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
_techSection(
|
||||
sheetContext,
|
||||
icon: Icons.location_on,
|
||||
title: AppLocalizations.of(context)!.location,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildLocationPreviewMap(
|
||||
sheetContext,
|
||||
messageLocationSnapshot.location,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_detailRow(
|
||||
sheetContext,
|
||||
label: AppLocalizations.of(
|
||||
context,
|
||||
)!.coordinates,
|
||||
value: messageLocationSnapshot
|
||||
.formattedCoordinates,
|
||||
onCopy: () => copyField(
|
||||
messageLocationSnapshot.formattedCoordinates,
|
||||
),
|
||||
),
|
||||
_detailRow(
|
||||
sheetContext,
|
||||
label: AppLocalizations.of(context)!.source,
|
||||
value: messageLocationSnapshot
|
||||
.technicalSourceLabel,
|
||||
),
|
||||
_detailRow(
|
||||
sheetContext,
|
||||
label: AppLocalizations.of(context)!.captured,
|
||||
value: _formatRfc3339(
|
||||
messageLocationSnapshot.sourceTimestamp ??
|
||||
messageLocationSnapshot.capturedAt,
|
||||
),
|
||||
onCopy: () => copyField(
|
||||
_formatRfc3339(
|
||||
messageLocationSnapshot.sourceTimestamp ??
|
||||
messageLocationSnapshot.capturedAt,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.message.lastEchoRssiDbm != null ||
|
||||
snrDb != null ||
|
||||
rssiDbm != null) ...[
|
||||
@@ -1333,6 +1382,53 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLocationPreviewMap(BuildContext context, LatLng location) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: SizedBox(
|
||||
height: 180,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
child: flutter_map.FlutterMap(
|
||||
options: flutter_map.MapOptions(
|
||||
initialCenter: location,
|
||||
initialZoom: 15,
|
||||
interactionOptions: const flutter_map.InteractionOptions(
|
||||
flags: flutter_map.InteractiveFlag.none,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
flutter_map.TileLayer(
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'com.meshcore.sar',
|
||||
),
|
||||
flutter_map.MarkerLayer(
|
||||
markers: [
|
||||
flutter_map.Marker(
|
||||
point: location,
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Icon(
|
||||
widget.message.isSentMessage
|
||||
? Icons.near_me
|
||||
: Icons.location_on,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _techSection(
|
||||
BuildContext context, {
|
||||
required IconData icon,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../../models/contact.dart';
|
||||
import '../../providers/app_provider.dart';
|
||||
import '../../providers/messages_provider.dart';
|
||||
import '../../utils/avatar_label_helper.dart';
|
||||
import '../common/contact_avatar.dart';
|
||||
@@ -259,6 +260,19 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
return _formatRelativeTime(context, lastActivityAt);
|
||||
}
|
||||
|
||||
IconData _channelLocationSharingIcon(ChannelLocationSharingMode mode) {
|
||||
return mode == ChannelLocationSharingMode.hardware
|
||||
? Icons.public_rounded
|
||||
: Icons.smartphone_rounded;
|
||||
}
|
||||
|
||||
Color _channelLocationSharingColor(
|
||||
BuildContext context,
|
||||
ChannelLocationSharingMode mode,
|
||||
) {
|
||||
return const Color(0xFF16A34A);
|
||||
}
|
||||
|
||||
Widget _buildTextSubtitle(
|
||||
BuildContext context,
|
||||
Contact contact,
|
||||
@@ -312,6 +326,9 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
MessagesProvider? messagesProvider,
|
||||
) {
|
||||
final previewData = _channelPreviewData(context, channel, messagesProvider);
|
||||
final sharingMode = context.watch<AppProvider>().channelLocationSharingModeForChannel(
|
||||
channel.publicKey.length > 1 ? channel.publicKey[1] : 0,
|
||||
);
|
||||
|
||||
return _buildRecipientCard(
|
||||
context: context,
|
||||
@@ -319,6 +336,7 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
contact: channel,
|
||||
title: channel.getLocalizedDisplayName(context),
|
||||
subtitle: _buildChannelSubtitle(context, channel, previewData),
|
||||
avatarMarkerMode: sharingMode,
|
||||
unreadCount: _unreadFor(channel),
|
||||
isSelected: _isSelected('channel', channel),
|
||||
compact: true,
|
||||
@@ -838,6 +856,7 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
required Contact contact,
|
||||
required String title,
|
||||
Widget? subtitle,
|
||||
ChannelLocationSharingMode? avatarMarkerMode,
|
||||
required int unreadCount,
|
||||
required bool isSelected,
|
||||
bool compact = false,
|
||||
@@ -846,6 +865,12 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
}) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final accentColor = _sectionColor(context, type);
|
||||
final markerColor = avatarMarkerMode == null
|
||||
? accentColor
|
||||
: _channelLocationSharingColor(context, avatarMarkerMode);
|
||||
final markerIcon = avatarMarkerMode == null
|
||||
? _typeIcon(contact)
|
||||
: _channelLocationSharingIcon(avatarMarkerMode);
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
@@ -886,14 +911,14 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
color: colorScheme.surface,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: accentColor.withValues(alpha: 0.3),
|
||||
color: markerColor.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(
|
||||
_typeIcon(contact),
|
||||
markerIcon,
|
||||
size: compact ? 9 : 10,
|
||||
color: accentColor,
|
||||
color: markerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -945,7 +970,7 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
?.copyWith(
|
||||
color: accentColor,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user