import 'dart:async'; import 'dart:ui'; import 'package:another_telephony/telephony.dart'; import '../utils/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:permission_handler/permission_handler.dart'; import '../utils/contact_helper.dart'; import '../utils/database_helper.dart'; import '../utils/protocol_helper.dart'; import '../utils/secure_crypto_helper.dart'; import '../utils/secure_messaging_service.dart'; import '../models/chat_model.dart'; import '../widgets/message_bubble.dart'; class MeshBackgroundPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint()..maskFilter = const MaskFilter.blur(BlurStyle.normal, 80); // Warm accent for groups paint.color = const Color(0xFFFFF3E0).withValues(alpha: 0.35); canvas.drawCircle(Offset(size.width * 0.8, size.height * 0.1), 160, paint); paint.color = const Color(0xFFF3E5F5).withValues(alpha: 0.35); canvas.drawCircle(Offset(size.width * 0.2, size.height * 0.8), 200, paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } class GroupMessageModel { int? id; String body; String? rawBody; int date; bool isMe; MessageStatus status; String? senderPhone; String? senderName; bool isSecure; bool canRetryDecryption; String? packetId; String? packetMode; bool isPendingMultipart; GroupMessageModel({ this.id, required this.body, this.rawBody, required this.date, required this.isMe, required this.status, this.senderPhone, this.senderName, this.isSecure = false, this.canRetryDecryption = false, this.packetId, this.packetMode, this.isPendingMultipart = false, }); } class GroupChatScreen extends StatefulWidget { final int groupId; final String groupName; const GroupChatScreen({ super.key, required this.groupId, required this.groupName, }); @override State createState() => _GroupChatScreenState(); } class _GroupChatScreenState extends State { final Telephony telephony = Telephony.instance; static const platform = MethodChannel('com.example.saba/sim_cards'); final TextEditingController _msgController = TextEditingController(); final TextEditingController _keyController = TextEditingController(); final ScrollController _scrollController = ScrollController(); Color get primaryColor => Theme.of(context).primaryColor; final Color backgroundColor = const Color(0xFFF5F7FA); List messages = []; List> members = []; List> _simCards = []; Map? _selectedSim; StreamSubscription? _msgSub; bool _loadingSims = true; String currentGroupName = ''; @override void initState() { super.initState(); currentGroupName = widget.groupName; SecureMessagingService.instance.currentChatPhone = 'group_${widget.groupId}'; DatabaseHelper.instance.markGroupAsRead(widget.groupId); _fetchSimCards(); _loadGroupKeyAndData(); _scrollController.addListener(_onScroll); _msgSub = SecureMessagingService.instance.messageStream.listen((data) { if (data['phone'] == 'group_${widget.groupId}') { if (!mounted) return; final body = data['body'] as String? ?? ''; final packetId = data['packetId'] as String?; final isMultipartComplete = data['isMultipartComplete'] as bool? ?? false; final isMeEvent = data['isMe'] as bool? ?? false; if (body == 'REFRESH') { _loadData(); return; } if (packetId != null) { setState(() { final idx = messages.indexWhere((m) => m.packetId == packetId); if (idx != -1) { messages[idx].body = body; messages[idx].isPendingMultipart = !isMultipartComplete; if (isMultipartComplete) { messages[idx].status = isMeEvent ? MessageStatus.sent : MessageStatus.received; } } else { // If it's a new fragmented message or just completed but not in list _loadData(); } }); } else { _loadData(); } } }); _keyController.addListener(() { if (mounted) _onKeyChanged(); }); } void _onScroll() { if (!_scrollController.hasClients) return; // Group chat might need pagination in the future, // for now we just monitor the state to prevent jumps. } @override void dispose() { if (SecureMessagingService.instance.currentChatPhone == 'group_${widget.groupId}') { SecureMessagingService.instance.currentChatPhone = null; } _msgSub?.cancel(); _msgController.dispose(); _keyController.dispose(); _scrollController.dispose(); super.dispose(); } Future _loadGroupKeyAndData() async { final groups = await DatabaseHelper.instance.getGroups(); final group = groups.cast?>().firstWhere( (g) => g?['id'] == widget.groupId, orElse: () => null, ); if (group != null && (group['group_key'] as String?)?.isNotEmpty == true) { _keyController.text = group['group_key'] as String; } await _loadData(); } void _onKeyChanged() { DatabaseHelper.instance.updateGroupKey(widget.groupId, _keyController.text); _loadData(); } bool _isSecurePlaceholderBody(String body) { return body.startsWith('پیام امن گروهی') || body.startsWith('بازگشایی پیام امن گروهی') || body.contains(' ::PAYLOAD::'); } String? _extractPayload(String body) { const separator = ' ::PAYLOAD::'; final splitIndex = body.lastIndexOf(separator); if (splitIndex == -1) return null; final payload = body.substring(splitIndex + separator.length).trim(); return payload.isEmpty ? null : payload; } Future _handleRetryDecryption(GroupMessageModel msg) async { String? payload = _extractPayload(msg.body); if (payload == null && msg.rawBody != null) { payload = ProtocolHelper.parseMessage(msg.rawBody!)['payload'] as String?; } if (payload == null) return; final keyController = TextEditingController(text: _keyController.text.trim()); showDialog( context: context, builder: (ctx) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), title: const Text('بازگشایی دستی پیام گروه'), content: TextField( controller: keyController, decoration: const InputDecoration( hintText: 'کلید گروه', border: OutlineInputBorder(), ), ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('لغو')), ElevatedButton( onPressed: () async { if (keyController.text.trim().isEmpty) return; final decrypted = await SecureMessagingService.instance.decryptWithKey( payload!, keyController.text.trim(), ); if (!mounted) return; Navigator.pop(ctx); if (decrypted != null) { _keyController.text = keyController.text.trim(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('پیام گروهی بازگشایی شد.')), ); _loadData(); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('کلید گروه نادرست است.')), ); } }, child: const Text('بازگشایی'), ), ], ), ); } Future> _resolveGroupMessageState(String rawBody, {bool isMe = false, bool dbIsSecure = false}) async { final parsed = ProtocolHelper.parseMessage(rawBody); final type = parsed['type'] as String; final key = _keyController.text.trim(); String body = rawBody; bool isSecure = dbIsSecure || type != 'plain' || rawBody.contains(' ::PAYLOAD::'); bool canRetryDecryption = false; if (type == 'sym' && parsed['isGroup'] == true) { final payload = parsed['payload'] as String?; if (payload != null && key.isNotEmpty) { final decrypted = await SecureMessagingService.instance.decryptWithKey(payload, key); if (decrypted != null) { body = decrypted; } else { final label = isMe ? 'ارسال شده' : 'دریافت شد'; body = 'بازگشایی پیام امن گروهی $label ناموفق بود. کلید گروه را بررسی کنید. ::PAYLOAD::$payload'; canRetryDecryption = true; } } else if (payload != null) { final label = isMe ? 'ارسال شده' : 'دریافت شد'; body = 'پیام امن گروهی $label. برای مشاهده، کلید گروه را وارد کنید. ::PAYLOAD::$payload'; canRetryDecryption = true; } } else if (type == 'sfra' && parsed['isGroup'] == true) { body = 'در حال دریافت قطعات... (${parsed['partNo']}/${parsed['totalParts']})'; isSecure = true; } else if (rawBody.contains(' ::PAYLOAD::')) { canRetryDecryption = true; } return { 'body': body, 'isSecure': isSecure, 'canRetryDecryption': canRetryDecryption, 'packetId': parsed['packetId'], 'packetMode': type == 'plain' ? null : (type == 'afrag' ? 'AE' : 'SYM'), 'isPendingMultipart': type == 'sfra' || type == 'afrag', }; } Future _fetchSimCards() async { if (!await Permission.phone.request().isGranted) { if (mounted) setState(() => _loadingSims = false); return; } try { final List result = await platform.invokeMethod('getSimCards'); final cleanList = result.map((e) { final rawMap = e as Map; return rawMap.map((key, value) => MapEntry(key.toString(), value)); }).toList(); if (!mounted) return; setState(() { _simCards = cleanList; if (_simCards.isNotEmpty) _selectedSim = _simCards.first; _loadingSims = false; }); } catch (_) { if (mounted) setState(() => _loadingSims = false); } } bool _isLoading = false; bool _needsReload = false; Future _loadData() async { if (_isLoading) { _needsReload = true; return; } _isLoading = true; _needsReload = false; try { final dbMessages = await DatabaseHelper.instance.getGroupMessages(widget.groupId); final dbMembers = await DatabaseHelper.instance.getGroupMembers(widget.groupId); final uiMessages = []; for (final m in dbMessages) { final raw = m['body'] as String; final senderPhone = m['sender_phone'] as String?; final senderName = senderPhone != null ? ContactHelper.getName(senderPhone) : 'من'; final dbIsSecure = (m['is_secure'] as int? ?? 0) == 1; final resolved = await _resolveGroupMessageState(raw, isMe: senderPhone == null, dbIsSecure: dbIsSecure); uiMessages.add(GroupMessageModel( id: m['id'] as int?, body: resolved['body'] as String, rawBody: raw, date: m['date'] as int, isMe: senderPhone == null, status: MessageStatus.received, senderPhone: senderPhone, senderName: senderName, isSecure: resolved['isSecure'] as bool, canRetryDecryption: resolved['canRetryDecryption'] as bool || _isSecurePlaceholderBody(resolved['body'] as String), packetId: resolved['packetId'] as String?, packetMode: resolved['packetMode'] as String?, isPendingMultipart: resolved['isPendingMultipart'] as bool? ?? false, )); } if (mounted) { setState(() { messages = uiMessages; members = dbMembers; }); } } finally { _isLoading = false; if (_needsReload) _loadData(); } } void _deleteMessage(int messageId) { showDialog( context: context, builder: (ctx) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), title: const Text('حذف پیام'), content: const Text('آیا مطمئن هستید؟'), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('لغو')), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red, foregroundColor: Colors.white), onPressed: () async { Navigator.pop(ctx); await DatabaseHelper.instance.deleteGroupMessage(messageId); _loadData(); }, child: const Text('حذف'), ), ], ), ); } void _editGroupName() { final nameController = TextEditingController(text: currentGroupName); showDialog( context: context, builder: (ctx) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), title: const Text('تغییر نام گروه'), content: TextField( controller: nameController, decoration: InputDecoration( labelText: 'نام جدید', border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), ), ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('لغو')), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: primaryColor, foregroundColor: Colors.white), onPressed: () async { if (nameController.text.trim().isEmpty) return; await DatabaseHelper.instance.updateGroupName(widget.groupId, nameController.text.trim()); if (!mounted) return; setState(() => currentGroupName = nameController.text.trim()); Navigator.pop(ctx); }, child: const Text('ذخیره'), ), ], ), ); } void _showGroupInfo() { showDialog( context: context, builder: (context) { return StatefulBuilder( builder: (context, setStateDialog) { return AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('مدیریت اعضا', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), IconButton( icon: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: primaryColor.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Icon(Icons.person_add, color: primaryColor, size: 20), ), onPressed: () { Navigator.pop(context); _addNewMember(); }, ), ], ), content: SizedBox( width: double.maxFinite, child: members.isEmpty ? const Center(child: Text('هیچ عضوی وجود ندارد', style: TextStyle(color: Colors.grey))) : ListView.separated( shrinkWrap: true, itemCount: members.length, separatorBuilder: (_, __) => const Divider(), itemBuilder: (_, index) { final member = members[index]; return ListTile( contentPadding: EdgeInsets.zero, leading: CircleAvatar( backgroundColor: Colors.orange.shade100, child: Text( (member['name'] as String)[0], style: TextStyle(color: Colors.orange.shade900), ), ), title: Text( member['name'] as String, style: const TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text(member['phone'] as String), trailing: IconButton( icon: const Icon(Icons.delete_outline, color: Colors.red), onPressed: () async { await DatabaseHelper.instance.removeGroupMember( widget.groupId, member['phone'] as String, ); final newMembers = await DatabaseHelper.instance.getGroupMembers(widget.groupId); if (!mounted) return; setState(() => members = newMembers); setStateDialog(() {}); }, ), ); }, ), ), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('بستن')), ], ); }, ); }, ); } Future _addNewMember() async { showDialog( context: context, barrierDismissible: false, builder: (_) => const Center(child: CircularProgressIndicator()), ); final contacts = await ContactHelper.getContactsLight(); if (mounted) Navigator.pop(context); if (contacts.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('مخاطبی یافت نشد'))); } return; } if (!mounted) return; final result = await showSearch?>( context: context, delegate: ContactsSearchDelegate(contacts), ); if (result == null) return; final added = await DatabaseHelper.instance.addMemberToGroup( widget.groupId, result['name']!, result['phone']!, ); if (!mounted) return; if (added) { await _loadData(); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('عضو جدید اضافه شد'))); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('این عضو قبلاً در گروه وجود دارد')), ); } } Future _sendGroupMessage() async { final text = _msgController.text.trim(); final key = _keyController.text.trim(); if (text.isEmpty) return; String? rawBody; if (key.isNotEmpty) { final crypto = SecureCryptoHelper(); rawBody = '@G:SYM|${await crypto.encryptSymmetric(text, key)}'; } final tempMsg = GroupMessageModel( body: text, rawBody: rawBody, date: DateTime.now().millisecondsSinceEpoch, isMe: true, status: MessageStatus.sending, senderName: 'من', isSecure: key.isNotEmpty, ); setState(() { messages.insert(0, tempMsg); _msgController.clear(); }); try { final groupMemberPhones = members.map((m) => m['phone'] as String).toList(); await SecureMessagingService.instance.sendGroupSecureMessage( widget.groupId, groupMemberPhones, text, key, ); if (!mounted) return; setState(() => tempMsg.status = MessageStatus.sent); Future.delayed(const Duration(milliseconds: 1200), _loadData); } catch (_) { if (!mounted) return; setState(() => tempMsg.status = MessageStatus.failed); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppTheme.darkBg, extendBodyBehindAppBar: true, resizeToAvoidBottomInset: true, appBar: PreferredSize( preferredSize: const Size.fromHeight(kToolbarHeight + 8), child: AppTheme.glassWrapper( radius: 0, sigma: 18, child: AppBar( backgroundColor: Colors.transparent, elevation: 0, leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.of(context).pop(), ), title: InkWell( onTap: _editGroupName, overlayColor: WidgetStateProperty.all(Colors.transparent), child: Row( mainAxisSize: MainAxisSize.min, children: [ Hero( tag: 'avatar_${currentGroupName}_group', child: Container( width: 38, height: 38, decoration: const BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( colors: [Colors.orange, Colors.orangeAccent], ), ), child: const Center( child: Icon(Icons.groups, color: Colors.white, size: 22), ), ), ), const SizedBox(width: 10), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(currentGroupName, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white)), Text('${members.length} عضو', style: const TextStyle(fontSize: 11, color: Colors.white70)), ], ), const SizedBox(width: 6), const Icon(Icons.edit_outlined, size: 14, color: Colors.white70), ], ), ), actions: [ IconButton( icon: const Icon(Icons.group_outlined, color: Colors.white), tooltip: 'مدیریت اعضا', onPressed: _showGroupInfo, ), ], ), ), ), body: Stack( children: [ Positioned.fill(child: CustomPaint(painter: MeshBackgroundPainter())), Column( children: [ SizedBox(height: MediaQuery.of(context).padding.top + kToolbarHeight + 8), Expanded( child: messages.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.mark_chat_unread_outlined, size: 60, color: Colors.grey[300]), const SizedBox(height: 10), Text('پیامی ارسال نشده است', style: TextStyle(color: Colors.grey[500])), ], ), ) : ListView.builder( key: const PageStorageKey('group_chat_list'), controller: _scrollController, reverse: true, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20), itemCount: messages.length, itemBuilder: (context, index) { final msg = messages[index]; return GestureDetector( onLongPress: () { if (msg.id != null) { _deleteMessage(msg.id!); } }, child: Column( crossAxisAlignment: msg.isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ if (!msg.isMe && msg.senderName != null) Padding( padding: const EdgeInsets.only(left: 12, bottom: 4), child: Text( msg.senderName!, style: const TextStyle( fontSize: 10, color: Colors.indigo, fontWeight: FontWeight.bold, ), ), ), MessageBubble( body: msg.body, rawBody: msg.rawBody, date: msg.date, isMe: msg.isMe, status: msg.status, isSecure: msg.isSecure, canRetryDecryption: msg.canRetryDecryption, onRetryDecryption: () => _handleRetryDecryption(msg), packetMode: msg.packetMode, isPendingMultipart: msg.isPendingMultipart, ), ], ), ); }, ), ), AppTheme.glassWrapper( radius: 0, sigma: 15, child: Container( padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.03), border: Border( top: BorderSide(color: Colors.white.withValues(alpha: 0.08), width: 0.5), ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ if (!_loadingSims && _simCards.length > 1) SizedOverflowBox( size: const Size.fromHeight(35), child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: _simCards.length, itemBuilder: (context, index) { final sim = _simCards[index]; return Padding( padding: const EdgeInsets.only(right: 8), child: ChoiceChip( label: Text(sim['displayName']?.toString() ?? 'SIM ${index + 1}'), selected: _selectedSim == sim, onSelected: (_) => setState(() => _selectedSim = sim), ), ); }, ), ), const SizedBox(height: 12), Row( children: [ Expanded( child: TextField( controller: _keyController, style: const TextStyle(color: Colors.white, fontSize: 13), decoration: InputDecoration( hintText: 'کلید امنیتی گروه (اختیاری)', hintStyle: const TextStyle(color: Colors.white30), prefixIcon: const Icon(Icons.vpn_key_rounded, size: 18, color: Colors.orangeAccent), filled: true, fillColor: Colors.white.withValues(alpha: 0.05), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), ), ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: TextField( controller: _msgController, minLines: 1, maxLines: 4, style: const TextStyle(color: Colors.white), decoration: InputDecoration( hintText: 'پیام گروهی...', hintStyle: const TextStyle(color: Colors.white30), filled: true, fillColor: Colors.white.withValues(alpha: 0.05), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), border: OutlineInputBorder( borderRadius: BorderRadius.circular(20), borderSide: BorderSide.none, ), ), ), ), const SizedBox(width: 12), Container( height: 48, width: 48, decoration: BoxDecoration( shape: BoxShape.circle, gradient: const LinearGradient( colors: [Colors.indigo, Colors.indigoAccent], ), boxShadow: [ BoxShadow( color: Colors.indigo.withValues(alpha: 0.3), blurRadius: 12, spreadRadius: 1, ), ], ), child: IconButton( icon: const Icon(Icons.send_rounded, color: Colors.white, size: 22), onPressed: _sendGroupMessage, ), ), ], ), ], ), ), ), ], ), ], ), ); } } class ContactsSearchDelegate extends SearchDelegate?> { final List contacts; ContactsSearchDelegate(this.contacts); @override String? get searchFieldLabel => 'جست‌وجو نام مخاطب...'; @override List? buildActions(BuildContext context) { return [ if (query.isNotEmpty) IconButton( icon: const Icon(Icons.clear), onPressed: () => query = '', ), ]; } @override Widget? buildLeading(BuildContext context) { return IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => close(context, null), ); } @override Widget buildResults(BuildContext context) => _buildList(context); @override Widget buildSuggestions(BuildContext context) => _buildList(context); Widget _buildList(BuildContext context) { final filtered = query.isEmpty ? contacts : contacts.where((c) => c.displayName.toLowerCase().contains(query.toLowerCase())).toList(); if (filtered.isEmpty) { return const Center(child: Text('نتیجه‌ای یافت نشد.')); } return ListView.builder( itemCount: filtered.length, itemBuilder: (context, index) { final contact = filtered[index]; return ListTile( leading: CircleAvatar( backgroundColor: Colors.indigo.shade100, child: Text( contact.displayName.isNotEmpty ? contact.displayName[0] : '?', style: TextStyle(color: Colors.indigo.shade900), ), ), title: Text(contact.displayName), onTap: () => _onContactSelect(context, contact.id), ); }, ); } Future _onContactSelect(BuildContext context, String contactId) async { showDialog( context: context, barrierDismissible: false, builder: (_) => const Center(child: CircularProgressIndicator()), ); try { final fullContact = await ContactHelper.getFullContact(contactId); if (context.mounted) Navigator.pop(context); if (fullContact == null || fullContact.phones.isEmpty) return; if (fullContact.phones.length == 1) { close(context, { 'name': fullContact.displayName, 'phone': fullContact.phones.first.number, }); } else { _showPhoneSelection(context, fullContact); } } catch (_) { if (context.mounted) Navigator.pop(context); } } void _showPhoneSelection(BuildContext context, Contact contact) { showDialog( context: context, builder: (_) => SimpleDialog( title: Text(contact.displayName), children: contact.phones .map( (p) => SimpleDialogOption( onPressed: () { Navigator.pop(context); close(context, { 'name': contact.displayName, 'phone': p.number, }); }, child: Text(p.number), ), ) .toList(), ), ); } }