mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Remove showDirectMessageDialog usage
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
|
import '../models/contact.dart';
|
||||||
import '../providers/contacts_provider.dart';
|
import '../providers/contacts_provider.dart';
|
||||||
import '../providers/app_provider.dart';
|
import '../providers/app_provider.dart';
|
||||||
import '../providers/connection_provider.dart';
|
import '../providers/connection_provider.dart';
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:latlong2/latlong.dart';
|
|||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
import '../models/sar_marker.dart';
|
import '../models/sar_marker.dart';
|
||||||
|
import '../widgets/common/contact_avatar.dart';
|
||||||
import '../widgets/map/location_pointer.dart';
|
import '../widgets/map/location_pointer.dart';
|
||||||
|
|
||||||
/// Centralized service for map marker management.
|
/// Centralized service for map marker management.
|
||||||
@@ -77,9 +78,15 @@ class MapMarkerService {
|
|||||||
// Marker icon
|
// Marker icon
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: getContactMarkerColor(contact, context),
|
color: Colors.white,
|
||||||
shape: BoxShape.circle,
|
shape: contact.type == ContactType.channel ||
|
||||||
border: Border.all(color: Colors.white, width: 2),
|
contact.type == ContactType.room
|
||||||
|
? BoxShape.rectangle
|
||||||
|
: BoxShape.circle,
|
||||||
|
borderRadius: contact.type == ContactType.channel ||
|
||||||
|
contact.type == ContactType.room
|
||||||
|
? BorderRadius.circular(14)
|
||||||
|
: null,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.3),
|
color: Colors.black.withValues(alpha: 0.3),
|
||||||
@@ -88,17 +95,8 @@ class MapMarkerService {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(2),
|
||||||
child: contact.roleEmoji != null
|
child: ContactAvatar(contact: contact, radius: 16),
|
||||||
? Text(
|
|
||||||
contact.roleEmoji!,
|
|
||||||
style: const TextStyle(fontSize: 18),
|
|
||||||
)
|
|
||||||
: Icon(
|
|
||||||
getContactMarkerIcon(contact),
|
|
||||||
color: Colors.white,
|
|
||||||
size: 18,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
// Name label (without emoji)
|
// Name label (without emoji)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import '../../providers/contacts_provider.dart';
|
|||||||
import '../../providers/map_provider.dart';
|
import '../../providers/map_provider.dart';
|
||||||
import '../../providers/app_provider.dart';
|
import '../../providers/app_provider.dart';
|
||||||
import 'contact_route_dialog.dart';
|
import 'contact_route_dialog.dart';
|
||||||
import 'direct_message_sheet.dart';
|
|
||||||
import 'room_login_sheet.dart';
|
import 'room_login_sheet.dart';
|
||||||
import '../../utils/location_formats.dart';
|
import '../../utils/location_formats.dart';
|
||||||
import '../../utils/toast_logger.dart';
|
import '../../utils/toast_logger.dart';
|
||||||
@@ -460,9 +459,9 @@ class ContactTile extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// In simple mode, tap directly opens message sheet for chat contacts
|
// In simple mode, tap directly opens the route editor for chat contacts
|
||||||
if (isSimpleMode && contact.type == ContactType.chat) {
|
if (isSimpleMode && contact.type == ContactType.chat) {
|
||||||
_showDirectMessageDialog(context, contact);
|
_showSetRouteDialog(context, contact);
|
||||||
} else if (isSimpleMode && contact.type == ContactType.repeater) {
|
} else if (isSimpleMode && contact.type == ContactType.repeater) {
|
||||||
// In simple mode, tapping a repeater jumps to the map
|
// In simple mode, tapping a repeater jumps to the map
|
||||||
_jumpToMapForRepeater(context, contact);
|
_jumpToMapForRepeater(context, contact);
|
||||||
@@ -515,15 +514,6 @@ class ContactTile extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showDirectMessageDialog(BuildContext context, Contact contact) {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
builder: (context) => DirectMessageSheet(contact: contact),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showRoomLoginDialog(BuildContext context, Contact contact) {
|
void _showRoomLoginDialog(BuildContext context, Contact contact) {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -1038,31 +1028,6 @@ class ContactTile extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
// Direct Message button for chat contacts
|
|
||||||
if (contact.type == ContactType.chat) ...[
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context); // Close details first
|
|
||||||
_showDirectMessageDialog(context, contact);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.message),
|
|
||||||
label: Text(
|
|
||||||
AppLocalizations.of(context)!.sendDirectMessage,
|
|
||||||
),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
||||||
backgroundColor: _getTypeColor(
|
|
||||||
contact.type,
|
|
||||||
context,
|
|
||||||
),
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
// Room Login button for room contacts (except Public Channel)
|
// Room Login button for room contacts (except Public Channel)
|
||||||
if (contact.type == ContactType.room &&
|
if (contact.type == ContactType.room &&
|
||||||
!contact.isPublicChannel) ...[
|
!contact.isPublicChannel) ...[
|
||||||
|
|||||||
@@ -1,452 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:geolocator/geolocator.dart';
|
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
|
||||||
import 'package:latlong2/latlong.dart';
|
|
||||||
import '../../models/contact.dart';
|
|
||||||
import '../../models/message.dart';
|
|
||||||
import '../../providers/connection_provider.dart';
|
|
||||||
import '../../providers/messages_provider.dart';
|
|
||||||
import '../../providers/app_provider.dart';
|
|
||||||
import '../../utils/toast_logger.dart';
|
|
||||||
import '../../l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
class DirectMessageSheet extends StatefulWidget {
|
|
||||||
final Contact contact;
|
|
||||||
|
|
||||||
const DirectMessageSheet({super.key, required this.contact});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<DirectMessageSheet> createState() => _DirectMessageSheetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DirectMessageSheetState extends State<DirectMessageSheet> {
|
|
||||||
final TextEditingController _textController = TextEditingController();
|
|
||||||
final FocusNode _focusNode = FocusNode();
|
|
||||||
int _characterCount = 0;
|
|
||||||
static const int _maxCharacters = 160;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_textController.addListener(_updateCharacterCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_textController.dispose();
|
|
||||||
_focusNode.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateCharacterCount() {
|
|
||||||
if (!mounted) return;
|
|
||||||
setState(() {
|
|
||||||
_characterCount = _textController.text.length;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Insert current GPS location at cursor position
|
|
||||||
Future<void> _insertCurrentLocation() async {
|
|
||||||
try {
|
|
||||||
// Check location permission
|
|
||||||
LocationPermission permission = await Geolocator.checkPermission();
|
|
||||||
if (permission == LocationPermission.denied) {
|
|
||||||
permission = await Geolocator.requestPermission();
|
|
||||||
if (permission == LocationPermission.denied) {
|
|
||||||
if (!mounted) return;
|
|
||||||
ToastLogger.error(context, 'Location permission denied');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (permission == LocationPermission.deniedForever) {
|
|
||||||
if (!mounted) return;
|
|
||||||
ToastLogger.error(context, 'Location permission permanently denied');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current position
|
|
||||||
final position = await Geolocator.getCurrentPosition(
|
|
||||||
locationSettings: const LocationSettings(
|
|
||||||
accuracy: LocationAccuracy.best,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Format location text
|
|
||||||
final locationText =
|
|
||||||
'📍 Lat: ${position.latitude.toStringAsFixed(5)}, Lon: ${position.longitude.toStringAsFixed(5)}';
|
|
||||||
|
|
||||||
// Check if adding location would exceed limit
|
|
||||||
final currentText = _textController.text;
|
|
||||||
if (currentText.length + locationText.length > _maxCharacters) {
|
|
||||||
if (!mounted) return;
|
|
||||||
ToastLogger.error(
|
|
||||||
context,
|
|
||||||
'Adding location would exceed 160 character limit',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert at cursor position or append
|
|
||||||
final selection = _textController.selection;
|
|
||||||
final newText = currentText.replaceRange(
|
|
||||||
selection.start >= 0 ? selection.start : currentText.length,
|
|
||||||
selection.end >= 0 ? selection.end : currentText.length,
|
|
||||||
locationText,
|
|
||||||
);
|
|
||||||
|
|
||||||
_textController.text = newText;
|
|
||||||
|
|
||||||
// Move cursor to end of inserted text
|
|
||||||
final newCursorPosition =
|
|
||||||
(selection.start >= 0 ? selection.start : currentText.length) +
|
|
||||||
locationText.length;
|
|
||||||
_textController.selection = TextSelection.fromPosition(
|
|
||||||
TextPosition(offset: newCursorPosition),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!mounted) return;
|
|
||||||
} catch (e) {
|
|
||||||
if (!mounted) return;
|
|
||||||
ToastLogger.error(context, 'Failed to get location: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _sendDirectMessage() async {
|
|
||||||
final text = _textController.text.trim();
|
|
||||||
if (text.isEmpty) return;
|
|
||||||
|
|
||||||
final connectionProvider = context.read<ConnectionProvider>();
|
|
||||||
final messagesProvider = context.read<MessagesProvider>();
|
|
||||||
|
|
||||||
if (!connectionProvider.deviceInfo.isConnected) {
|
|
||||||
if (!mounted) return;
|
|
||||||
ToastLogger.error(
|
|
||||||
context,
|
|
||||||
AppLocalizations.of(context)!.notConnectedToDevice,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create message ID
|
|
||||||
final messageId = '${DateTime.now().millisecondsSinceEpoch}_dm_sent';
|
|
||||||
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
|
||||||
|
|
||||||
// Get current device's public key (first 6 bytes)
|
|
||||||
final devicePublicKey = connectionProvider.deviceInfo.publicKey;
|
|
||||||
final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6);
|
|
||||||
|
|
||||||
// Create sent message object with recipient public key for retry support
|
|
||||||
final sentMessage = Message(
|
|
||||||
id: messageId,
|
|
||||||
messageType: MessageType.contact,
|
|
||||||
senderPublicKeyPrefix: senderPublicKeyPrefix,
|
|
||||||
pathLen: 0,
|
|
||||||
textType: MessageTextType.plain,
|
|
||||||
senderTimestamp: timestamp,
|
|
||||||
text: text,
|
|
||||||
receivedAt: DateTime.now(),
|
|
||||||
deliveryStatus: MessageDeliveryStatus.sending,
|
|
||||||
recipientPublicKey:
|
|
||||||
widget.contact.publicKey, // Store recipient for retry
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add to messages list with "sending" status
|
|
||||||
// Pass contact for retry logic
|
|
||||||
messagesProvider.addSentMessage(sentMessage, contact: widget.contact);
|
|
||||||
|
|
||||||
// Send direct message to contact (include contact for path logging)
|
|
||||||
final sentSuccessfully = await connectionProvider.sendTextMessage(
|
|
||||||
contactPublicKey: widget.contact.publicKey,
|
|
||||||
text: text,
|
|
||||||
messageId: messageId, // Pass message ID for tracking
|
|
||||||
contact: widget.contact,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!sentSuccessfully) {
|
|
||||||
// Mark message as failed if sending failed
|
|
||||||
messagesProvider.markMessageFailed(messageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
_textController.clear();
|
|
||||||
_focusNode.unfocus();
|
|
||||||
|
|
||||||
if (!mounted) return;
|
|
||||||
Navigator.pop(context); // Close the dialog
|
|
||||||
} catch (e) {
|
|
||||||
if (!mounted) return;
|
|
||||||
ToastLogger.error(
|
|
||||||
context,
|
|
||||||
AppLocalizations.of(context)!.failedToSend(e.toString()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final colorScheme = theme.colorScheme;
|
|
||||||
final appProvider = context.watch<AppProvider>();
|
|
||||||
final isSimpleMode = appProvider.isSimpleMode;
|
|
||||||
final contactLocation = widget.contact.displayLocation;
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
height: MediaQuery.of(context).size.height * 0.9,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: colorScheme.surface,
|
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// Header
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: colorScheme.surfaceContainerHighest,
|
|
||||||
borderRadius: const BorderRadius.vertical(
|
|
||||||
top: Radius.circular(20),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.arrow_back, color: colorScheme.onSurface),
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
AppLocalizations.of(context)!.directMessage,
|
|
||||||
style: TextStyle(
|
|
||||||
color: colorScheme.onSurface,
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
widget.contact.displayName,
|
|
||||||
style: TextStyle(
|
|
||||||
color: colorScheme.onSurfaceVariant,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 48), // Spacer to keep title centered
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Mini map in simple mode (scrollable content)
|
|
||||||
Expanded(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
if (isSimpleMode && contactLocation != null) ...[
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
// Hide keyboard when tapping on map
|
|
||||||
_focusNode.unfocus();
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
height: 200,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: colorScheme.outline),
|
|
||||||
),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: FlutterMap(
|
|
||||||
options: MapOptions(
|
|
||||||
initialCenter: LatLng(
|
|
||||||
contactLocation.latitude,
|
|
||||||
contactLocation.longitude,
|
|
||||||
),
|
|
||||||
initialZoom: 13.0,
|
|
||||||
interactionOptions: const InteractionOptions(
|
|
||||||
flags:
|
|
||||||
InteractiveFlag.pinchZoom |
|
|
||||||
InteractiveFlag.drag,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
TileLayer(
|
|
||||||
urlTemplate:
|
|
||||||
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
||||||
userAgentPackageName: 'com.meshcore.sar',
|
|
||||||
),
|
|
||||||
MarkerLayer(
|
|
||||||
markers: [
|
|
||||||
Marker(
|
|
||||||
point: LatLng(
|
|
||||||
contactLocation.latitude,
|
|
||||||
contactLocation.longitude,
|
|
||||||
),
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
child: Icon(
|
|
||||||
Icons.location_on,
|
|
||||||
color: colorScheme.primary,
|
|
||||||
size: 40,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
// Location coordinates
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.gps_fixed,
|
|
||||||
size: 14,
|
|
||||||
color: colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
'${contactLocation.latitude.toStringAsFixed(5)}, ${contactLocation.longitude.toStringAsFixed(5)}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: colorScheme.onSurfaceVariant,
|
|
||||||
fontSize: 12,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Message input
|
|
||||||
Container(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
left: 16,
|
|
||||||
right: 16,
|
|
||||||
top: 16,
|
|
||||||
bottom: 16 + MediaQuery.of(context).viewInsets.bottom,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: colorScheme.surfaceContainerHighest,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
TextField(
|
|
||||||
controller: _textController,
|
|
||||||
focusNode: _focusNode,
|
|
||||||
maxLength: _maxCharacters,
|
|
||||||
maxLines: 3,
|
|
||||||
autofocus: true,
|
|
||||||
maxLengthEnforcement: MaxLengthEnforcement.enforced,
|
|
||||||
style: TextStyle(color: colorScheme.onSurface),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: AppLocalizations.of(context)!.typeYourMessage,
|
|
||||||
hintStyle: TextStyle(color: colorScheme.onSurfaceVariant),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide(color: colorScheme.outline),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide(color: colorScheme.outline),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: colorScheme.primary,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.all(16),
|
|
||||||
counterText: '', // Hide default counter
|
|
||||||
),
|
|
||||||
textInputAction: TextInputAction.send,
|
|
||||||
onSubmitted: (_) => _sendDirectMessage(),
|
|
||||||
),
|
|
||||||
// Always-visible character counter
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 4,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'$_characterCount / $_maxCharacters',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: _characterCount > 155
|
|
||||||
? Colors.red
|
|
||||||
: (_characterCount > 140
|
|
||||||
? Colors.orange
|
|
||||||
: colorScheme.onSurfaceVariant),
|
|
||||||
fontWeight: _characterCount > 140
|
|
||||||
? FontWeight.bold
|
|
||||||
: FontWeight.normal,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
// Location and Send buttons
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: _insertCurrentLocation,
|
|
||||||
icon: const Icon(Icons.my_location, size: 18),
|
|
||||||
label: Text(AppLocalizations.of(context)!.myLocation),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
side: BorderSide(color: colorScheme.outline),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: _textController.text.trim().isEmpty
|
|
||||||
? null
|
|
||||||
: _sendDirectMessage,
|
|
||||||
icon: const Icon(Icons.send),
|
|
||||||
label: Text(
|
|
||||||
AppLocalizations.of(context)!.sendDirectMessage,
|
|
||||||
),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
||||||
backgroundColor: colorScheme.primary,
|
|
||||||
foregroundColor: colorScheme.onPrimary,
|
|
||||||
disabledBackgroundColor:
|
|
||||||
colorScheme.surfaceContainerHighest,
|
|
||||||
disabledForegroundColor: colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,6 @@ import '../../providers/connection_provider.dart';
|
|||||||
import '../../providers/drawing_provider.dart';
|
import '../../providers/drawing_provider.dart';
|
||||||
import '../../providers/voice_provider.dart';
|
import '../../providers/voice_provider.dart';
|
||||||
import '../../providers/image_provider.dart' as ip;
|
import '../../providers/image_provider.dart' as ip;
|
||||||
import '../contacts/direct_message_sheet.dart';
|
|
||||||
import '../drawing_minimap_preview.dart';
|
import '../drawing_minimap_preview.dart';
|
||||||
import '../../models/ble_packet_log.dart';
|
import '../../models/ble_packet_log.dart';
|
||||||
import '../../services/sar_template_service.dart';
|
import '../../services/sar_template_service.dart';
|
||||||
@@ -193,19 +192,12 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showMessageOptions(BuildContext context) {
|
void _showMessageOptions(BuildContext context) {
|
||||||
// Determine if this is own message
|
|
||||||
final connectionProvider = context.read<ConnectionProvider>();
|
final connectionProvider = context.read<ConnectionProvider>();
|
||||||
final selfPublicKey = connectionProvider.deviceInfo.publicKey;
|
final selfPublicKey = connectionProvider.deviceInfo.publicKey;
|
||||||
final isOwnMessage =
|
final isOwnMessage =
|
||||||
widget.message.isSentMessage ||
|
widget.message.isSentMessage ||
|
||||||
widget.message.isFromSelf(selfPublicKey);
|
widget.message.isFromSelf(selfPublicKey);
|
||||||
|
|
||||||
// Check if we can reply to this message (must be contact message from someone else)
|
|
||||||
final canReply =
|
|
||||||
widget.message.isContactMessage &&
|
|
||||||
!isOwnMessage &&
|
|
||||||
widget.message.senderPublicKeyPrefix != null;
|
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
@@ -217,16 +209,6 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Reply option (only for contact messages from others)
|
|
||||||
if (canReply)
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.reply),
|
|
||||||
title: Text(AppLocalizations.of(context)!.reply),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
_showReplySheet(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// Copy text option
|
// Copy text option
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.copy),
|
leading: const Icon(Icons.copy),
|
||||||
@@ -1284,44 +1266,6 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
return raw.sublist(5, 5 + pathLen);
|
return raw.sublist(5, 5 + pathLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showReplySheet(BuildContext context) {
|
|
||||||
// Find the sender contact by public key prefix
|
|
||||||
final contactsProvider = context.read<ContactsProvider>();
|
|
||||||
|
|
||||||
if (widget.message.senderPublicKeyPrefix == null) {
|
|
||||||
ToastLogger.error(context, 'Cannot reply: sender information missing');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find contact by public key prefix (first 6 bytes)
|
|
||||||
final senderKeyHex = widget.message.senderPublicKeyPrefix!
|
|
||||||
.sublist(
|
|
||||||
0,
|
|
||||||
widget.message.senderPublicKeyPrefix!.length < 6
|
|
||||||
? widget.message.senderPublicKeyPrefix!.length
|
|
||||||
: 6,
|
|
||||||
)
|
|
||||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
final senderContact = contactsProvider.contacts.where((c) {
|
|
||||||
return c.publicKeyHex.startsWith(senderKeyHex);
|
|
||||||
}).firstOrNull;
|
|
||||||
|
|
||||||
if (senderContact == null) {
|
|
||||||
ToastLogger.error(context, 'Cannot reply: contact not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show direct message sheet for the sender
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
builder: (context) => DirectMessageSheet(contact: senderContact),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showDeleteConfirmation(BuildContext context) {
|
void _showDeleteConfirmation(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
showDialog(
|
showDialog(
|
||||||
|
|||||||
63
test/services/map_marker_service_test.dart
Normal file
63
test/services/map_marker_service_test.dart
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:meshcore_sar_app/models/contact.dart';
|
||||||
|
import 'package:meshcore_sar_app/services/map_marker_service.dart';
|
||||||
|
import 'package:meshcore_sar_app/widgets/common/contact_avatar.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
Contact buildContact({
|
||||||
|
required String name,
|
||||||
|
required ContactType type,
|
||||||
|
required int advLat,
|
||||||
|
required int advLon,
|
||||||
|
}) {
|
||||||
|
return Contact(
|
||||||
|
publicKey: Uint8List(32),
|
||||||
|
type: type,
|
||||||
|
flags: 0,
|
||||||
|
outPathLen: 0,
|
||||||
|
outPath: Uint8List(0),
|
||||||
|
advName: name,
|
||||||
|
lastAdvert: DateTime.now().millisecondsSinceEpoch,
|
||||||
|
advLat: advLat,
|
||||||
|
advLon: advLon,
|
||||||
|
lastMod: DateTime.now().millisecondsSinceEpoch,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('contact map markers render shared contact avatars', (tester) async {
|
||||||
|
final service = MapMarkerService();
|
||||||
|
final contact = buildContact(
|
||||||
|
name: 'John Smith',
|
||||||
|
type: ContactType.chat,
|
||||||
|
advLat: (46.0569 * 1e6).round(),
|
||||||
|
advLon: (14.5058 * 1e6).round(),
|
||||||
|
);
|
||||||
|
|
||||||
|
late Widget markerChild;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final markers = service.generateContactMarkers(
|
||||||
|
contacts: [contact],
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
markerChild = markers.single.child;
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(home: Scaffold(body: Center(child: markerChild))),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(ContactAvatar), findsOneWidget);
|
||||||
|
expect(find.text('JS'), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user