import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../providers/messages_provider.dart'; import '../providers/contacts_provider.dart'; import '../providers/map_provider.dart'; import '../providers/connection_provider.dart'; import '../models/message.dart'; import '../models/contact.dart'; class MessagesTab extends StatefulWidget { final VoidCallback onNavigateToMap; const MessagesTab({super.key, required this.onNavigateToMap}); @override State createState() => _MessagesTabState(); } class _MessagesTabState extends State { final TextEditingController _textController = TextEditingController(); final FocusNode _focusNode = FocusNode(); Contact? _selectedContact; 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() { setState(() { _characterCount = _textController.text.length; }); } Future _sendMessage() async { final text = _textController.text.trim(); if (text.isEmpty) return; final connectionProvider = context.read(); if (!connectionProvider.deviceInfo.isConnected) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Not connected to device'), backgroundColor: Colors.red, ), ); return; } if (_selectedContact == null) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Please select a recipient'), backgroundColor: Colors.orange, ), ); return; } try { // Send to channel/room or direct contact if (_selectedContact!.isRoom) { // For rooms/channels, use the first byte of outPath as channel index final channelIdx = _selectedContact!.outPath.isNotEmpty ? _selectedContact!.outPath[0] : 0; await connectionProvider.sendChannelMessage( channelIdx: channelIdx, text: text, ); } else { // For direct contacts (chat type) await connectionProvider.sendTextMessage( contactPublicKey: _selectedContact!.publicKey, text: text, ); } _textController.clear(); _focusNode.unfocus(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Message sent'), backgroundColor: Colors.green, duration: Duration(seconds: 1), ), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Failed to send: $e'), backgroundColor: Colors.red, ), ); } } void _showContactSelector() { final contactsProvider = context.read(); final allContacts = contactsProvider.contacts; if (allContacts.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('No contacts available'), backgroundColor: Colors.orange, ), ); return; } // Group contacts by type final chatContacts = contactsProvider.chatContacts; final rooms = contactsProvider.rooms; final repeaters = contactsProvider.repeaters; showModalBottomSheet( context: context, builder: (context) => Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.all(16), child: Text( 'Select Recipient', style: Theme.of(context).textTheme.titleLarge, ), ), const Divider(height: 1), Flexible( child: ListView( shrinkWrap: true, children: [ // Chat contacts section if (chatContacts.isNotEmpty) ...[ Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), child: Text( 'Team Members', style: Theme.of(context).textTheme.labelLarge?.copyWith( color: Theme.of(context).colorScheme.primary, ), ), ), ...chatContacts.map((contact) => _buildContactTile( contact: contact, icon: Icons.person, color: Colors.blue, )), ], // Rooms/Channels section if (rooms.isNotEmpty) ...[ Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), child: Text( 'Channels', style: Theme.of(context).textTheme.labelLarge?.copyWith( color: Theme.of(context).colorScheme.primary, ), ), ), ...rooms.map((contact) => _buildContactTile( contact: contact, icon: Icons.tag, color: Colors.purple, )), ], // Repeaters section (informational only) if (repeaters.isNotEmpty) ...[ Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), child: Text( 'Repeaters (Read-only)', style: Theme.of(context).textTheme.labelLarge?.copyWith( color: Colors.grey, ), ), ), ...repeaters.map((contact) => ListTile( enabled: false, leading: CircleAvatar( backgroundColor: Colors.grey, child: const Icon(Icons.router, color: Colors.white, size: 20), ), title: Text(contact.advName), subtitle: Text(contact.timeSinceLastSeen), )), ], ], ), ), ], ), ); } Widget _buildContactTile({ required Contact contact, required IconData icon, required Color color, }) { return ListTile( leading: CircleAvatar( backgroundColor: color, child: Icon(icon, color: Colors.white, size: 20), ), title: Text(contact.advName), subtitle: Text(contact.timeSinceLastSeen), trailing: _selectedContact?.publicKeyHex == contact.publicKeyHex ? const Icon(Icons.check_circle, color: Colors.green) : null, onTap: () { setState(() { _selectedContact = contact; }); Navigator.pop(context); }, ); } @override Widget build(BuildContext context) { return Consumer( builder: (context, messagesProvider, child) { final messages = messagesProvider.getRecentMessages(count: 100); return Column( children: [ // Messages list Expanded( child: messages.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.message_outlined, size: 64, color: Theme.of(context).disabledColor, ), const SizedBox(height: 16), Text( 'No messages yet', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( 'Connect to a device to start receiving messages', style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.center, ), ], ), ) : ListView.builder( reverse: true, padding: const EdgeInsets.all(8), itemCount: messages.length, itemBuilder: (context, index) { final message = messages[index]; return _MessageBubble( message: message, onTap: message.isSarMarker && message.sarGpsCoordinates != null ? () { final mapProvider = context.read(); mapProvider.navigateToLocation( location: message.sarGpsCoordinates!, zoom: 15.0, ); widget.onNavigateToMap(); } : null, ); }, ), ), // Message input area Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, border: Border( top: BorderSide( color: Theme.of(context).dividerColor, width: 1, ), ), ), padding: const EdgeInsets.fromLTRB(4, 4, 4, 4), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Contact selector (compact) if (_selectedContact != null) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3, ), margin: const EdgeInsets.only(bottom: 4), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Icon( _selectedContact!.isRoom ? Icons.tag : Icons.person, size: 14, ), const SizedBox(width: 6), Expanded( child: Text( _selectedContact!.advName, style: const TextStyle( fontWeight: FontWeight.w500, fontSize: 12, ), overflow: TextOverflow.ellipsis, ), ), GestureDetector( onTap: () { setState(() { _selectedContact = null; }); }, child: const Padding( padding: EdgeInsets.all(4), child: Icon(Icons.close, size: 14), ), ), ], ), ), // Text input row Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ // Contact selector button (compact) IconButton( icon: const Icon(Icons.contacts, size: 20), onPressed: _showContactSelector, tooltip: 'Select contact', padding: const EdgeInsets.all(8), constraints: const BoxConstraints( minWidth: 36, minHeight: 36, ), ), // Text field (compact) Expanded( child: TextField( controller: _textController, focusNode: _focusNode, maxLength: _maxCharacters, maxLines: null, maxLengthEnforcement: MaxLengthEnforcement.enforced, style: const TextStyle(fontSize: 14), decoration: InputDecoration( hintText: 'Message...', hintStyle: const TextStyle(fontSize: 14), border: OutlineInputBorder( borderRadius: BorderRadius.circular(20), ), contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), isDense: true, counterText: '$_characterCount/$_maxCharacters', counterStyle: TextStyle( fontSize: 10, color: _characterCount > _maxCharacters * 0.9 ? Colors.orange : Theme.of(context).textTheme.bodySmall?.color, ), ), textInputAction: TextInputAction.send, onSubmitted: (_) => _sendMessage(), ), ), // Send button (compact) IconButton( icon: const Icon(Icons.send, size: 20), onPressed: _textController.text.trim().isEmpty ? null : _sendMessage, color: Theme.of(context).colorScheme.primary, padding: const EdgeInsets.all(8), constraints: const BoxConstraints( minWidth: 36, minHeight: 36, ), ), ], ), ], ), ), ], ); }, ); } } class _MessageBubble extends StatelessWidget { final Message message; final VoidCallback? onTap; const _MessageBubble({ required this.message, this.onTap, }); @override Widget build(BuildContext context) { final isSarMarker = message.isSarMarker; return GestureDetector( onTap: onTap, child: Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isSarMarker ? _getSarMarkerColor(context) : Theme.of(context).colorScheme.surfaceVariant, borderRadius: BorderRadius.circular(12), border: isSarMarker ? Border.all( color: Theme.of(context).colorScheme.primary, width: 2, ) : null, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header: Sender and time Row( children: [ if (message.isChannelMessage) const Icon(Icons.tag, size: 16) else const Icon(Icons.person, size: 16), const SizedBox(width: 4), Text( message.displaySender, style: Theme.of(context).textTheme.labelMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const Spacer(), if (isSarMarker) Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary, borderRadius: BorderRadius.circular(4), ), child: Text( 'SAR', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Theme.of(context).colorScheme.onPrimary, fontWeight: FontWeight.bold, ), ), ), const SizedBox(width: 8), Text( message.timeAgo, style: Theme.of(context).textTheme.labelSmall, ), ], ), const SizedBox(height: 8), // SAR marker content if (isSarMarker && message.sarMarkerType != null) ...[ Row( children: [ Text( message.sarMarkerType!.emoji, style: const TextStyle(fontSize: 32), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( message.sarMarkerType!.displayName, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), if (message.sarGpsCoordinates != null) Text( '${message.sarGpsCoordinates!.latitude.toStringAsFixed(5)}, ${message.sarGpsCoordinates!.longitude.toStringAsFixed(5)}', style: Theme.of(context).textTheme.bodySmall, ), ], ), ), Icon( Icons.chevron_right, color: Theme.of(context).colorScheme.primary, ), ], ), const SizedBox(height: 4), Text( 'Tap to view on map', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Theme.of(context).colorScheme.primary, fontStyle: FontStyle.italic, ), ), ] // Regular message content else Text( message.text, style: Theme.of(context).textTheme.bodyMedium, ), ], ), ), ); } Color _getSarMarkerColor(BuildContext context) { return Theme.of(context).colorScheme.primaryContainer; } }