import 'dart:async'; import 'dart:math'; import 'dart:ui'; import 'package:another_telephony/telephony.dart'; import 'package:flutter/foundation.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 '../models/chat_model.dart'; import '../utils/app_theme.dart'; import '../utils/contact_helper.dart'; import '../utils/database_helper.dart'; import '../utils/message_processor.dart'; import '../utils/protocol_helper.dart'; import '../utils/secure_messaging_service.dart'; import '../widgets/message_bubble.dart'; class MeshBackgroundPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { // Optimization for Raspberry Pi 3: Use RadialGradient instead of MaskFilter blur // which is often hardware-accelerated and much cheaper than multi-pass blur. final Paint paint1 = Paint() ..shader = RadialGradient( colors: [ const Color(0xFF7000FF).withValues(alpha: 0.12), const Color(0xFF7000FF).withValues(alpha: 0.0), ], ).createShader(Rect.fromCircle( center: Offset(size.width * 0.2, size.height * 0.15), radius: 300)); canvas.drawCircle( Offset(size.width * 0.2, size.height * 0.15), 300, paint1); final Paint paint2 = Paint() ..shader = RadialGradient( colors: [ const Color(0xFF00D2FF).withValues(alpha: 0.1), const Color(0xFF00D2FF).withValues(alpha: 0.0), ], ).createShader(Rect.fromCircle( center: Offset(size.width * 0.8, size.height * 0.8), radius: 400)); canvas.drawCircle(Offset(size.width * 0.8, size.height * 0.8), 400, paint2); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } class _MessageCandidate { final ChatModel message; final bool preferCandidate; const _MessageCandidate( this.message, { this.preferCandidate = false, }); } class ChatScreen extends StatefulWidget { final String address; const ChatScreen({super.key, required this.address}); @override State createState() => _ChatScreenState(); } class _ChatScreenState extends State { final Telephony telephony = Telephony.instance; static const platform = MethodChannel('com.example.saba/sim_cards'); static const nativeChannel = MethodChannel('com.example.saba/sms_native'); static const asymmetricModeLabel = "رمزنگاری غیر متقارن (طولانی‌تر و امن‌تر)"; final TextEditingController _msgController = TextEditingController(); Color get primaryColor => Theme.of(context).primaryColor; Color get backgroundColor => const Color(0xFFF5F7FA); final ScrollController _scrollController = ScrollController(); final ValueNotifier> _messageNotifier = ValueNotifier([]); List get messages => _messageNotifier.value; set messages(List val) => _messageNotifier.value = val; List> _simCards = []; Map? _selectedSim; bool _loadingSims = true; String displayName = ""; String _selectedSecurityLevel = 'normal'; Map? contactInfo; StreamSubscription? _messageSubscription; String? _dismissedBannerText; bool _isLoading = false; Timer? _debounceTimer; int _offset = 0; static const int _limit = 25; static const int _liveMessageGraceMs = 15000; bool _hasMore = true; int _compareMessages(ChatModel a, ChatModel b) { final dateComp = b.date.compareTo(a.date); if (dateComp != 0) return dateComp; final idComp = (b.id ?? -1).compareTo(a.id ?? -1); if (idComp != 0) return idComp; final packetComp = (_packetKey(b.packetId, b.packetMode) ?? '') .compareTo(_packetKey(a.packetId, a.packetMode) ?? ''); if (packetComp != 0) return packetComp; final isMeComp = (b.isMe ? 1 : 0).compareTo(a.isMe ? 1 : 0); if (isMeComp != 0) return isMeComp; final bodyComp = (b.rawBody ?? b.body).compareTo(a.rawBody ?? a.body); if (bodyComp != 0) return bodyComp; return b.status.index.compareTo(a.status.index); } int _messageQuality(ChatModel message) { var score = 0; if (message.id != null) score += 40; if (_packetKey(message.packetId, message.packetMode) != null) score += 8; if ((message.rawBody?.isNotEmpty ?? false)) score += 3; if ((message.rawViewBody?.isNotEmpty ?? false)) score += 2; if ((message.encryptedPayload?.isNotEmpty ?? false)) score += 4; if (message.isSecure) score += 3; if (!message.isPendingMultipart) score += 6; if (message.status == MessageStatus.sent || message.status == MessageStatus.received) { score += 2; } if (message.status == MessageStatus.sending) score -= 2; if (message.status == MessageStatus.failed) score -= 1; if (_isSecurePlaceholderBody(message.body)) score -= 1; return score; } void _publishMessages(List items) { final sorted = List.from(items)..sort(_compareMessages); _messageNotifier.value = sorted; } String? _packetKey(String? packetId, [String? packetMode]) { if (packetId == null || packetId.isEmpty) return null; final mode = (packetMode == null || packetMode.isEmpty) ? 'UNK' : packetMode; return '$mode::$packetId'; } ChatModel _cloneMessage(ChatModel source) { return ChatModel( id: source.id, localId: source.localId, body: source.body, rawBody: source.rawBody, rawViewBody: source.rawViewBody, encryptedPayload: source.encryptedPayload, statusLabel: source.statusLabel, packetId: source.packetId, packetMode: source.packetMode, rawPacketId: source.rawPacketId, date: source.date, isMe: source.isMe, status: source.status, isSecure: source.isSecure, canRetryDecryption: source.canRetryDecryption, isPendingMultipart: source.isPendingMultipart, isRead: source.isRead, ); } String _fallbackMessageKey(ChatModel message) { return 'fallback::${message.isMe ? 1 : 0}::${message.date}::${message.body.hashCode}'; } String? _normalizedPayloadSignature(String? payload) { final normalized = (payload ?? '').replaceAll(RegExp(r'\s+'), '').trim(); return normalized.isEmpty ? null : normalized; } String? _contentSignature(ChatModel message) { final rawSignature = (message.rawBody ?? '').trim(); final bodySignature = message.body.trim(); final contentSignature = rawSignature.isNotEmpty ? rawSignature : bodySignature; if (contentSignature.isEmpty || _isSecurePlaceholderBody(contentSignature) || _isProgressBody(contentSignature)) { return null; } return contentSignature; } int _timeBucket(int date, [int bucketMs = 5000]) { if (date <= 0) return 0; return date ~/ bucketMs; } List _messageAliases(ChatModel message) { final aliases = {}; if (message.localId != null && message.localId!.isNotEmpty) { aliases.add('local::${message.localId}'); } if (message.id != null) { aliases.add('db::${message.id}'); } final packetKey = _packetKey(message.packetId, message.packetMode); if (packetKey != null) { aliases.add('packet::$packetKey'); } final payload = _normalizedPayloadSignature(message.encryptedPayload); if (payload != null) { final mode = (message.packetMode ?? 'SEC').trim(); aliases.add('payload::$mode::${message.isMe ? 1 : 0}::$payload'); aliases.add('payload::$mode::$payload'); } final rawBody = (message.rawBody ?? '').trim(); if (rawBody.isNotEmpty) { aliases.add('raw::${message.isMe ? 1 : 0}::$rawBody'); final parsed = ProtocolHelper.parseMessage(rawBody); final rawPayload = _normalizedPayloadSignature(parsed['payload'] as String?); final rawType = parsed['type'] as String? ?? 'plain'; if (rawPayload != null) { final mode = (message.packetMode ?? (rawType == 'asym' || rawType == 'afrag' ? 'AE' : 'SYM')) .trim(); aliases.add('payload::$mode::${message.isMe ? 1 : 0}::$rawPayload'); aliases.add('payload::$mode::$rawPayload'); } } final content = _contentSignature(message); if (content != null) { aliases.add( 'body::${message.isMe ? 1 : 0}::${_timeBucket(message.date)}::$content'); } return aliases.toList(growable: false); } bool _hasAliasOverlap(Set a, Set b) { for (final key in a) { if (b.contains(key)) return true; } return false; } bool _isProgressBody(String body) { return body.contains('در حال دریافت قطعات') || body.contains('در حال ارسال قطعات') || body.contains('باقی قطعات پیام نرسیده‌اند'); } String _pickPreferredBody(String primaryBody, String secondaryBody) { final primary = primaryBody.trim(); final secondary = secondaryBody.trim(); if (primary.isEmpty) return secondaryBody; if (secondary.isEmpty) return primaryBody; final primaryProgress = _isProgressBody(primaryBody); final secondaryProgress = _isProgressBody(secondaryBody); if (primaryProgress != secondaryProgress) { return primaryProgress ? secondaryBody : primaryBody; } final primaryPlaceholder = _isSecurePlaceholderBody(primaryBody); final secondaryPlaceholder = _isSecurePlaceholderBody(secondaryBody); if (primaryPlaceholder != secondaryPlaceholder) { return primaryPlaceholder ? secondaryBody : primaryBody; } return primary.length >= secondary.length ? primaryBody : secondaryBody; } String? _pickPreferredString(String? primary, String? secondary) { final a = (primary ?? '').trim(); final b = (secondary ?? '').trim(); if (a.isEmpty) return b.isEmpty ? null : secondary; if (b.isEmpty) return primary; return a.length >= b.length ? primary : secondary; } MessageStatus _pickPreferredStatus(ChatModel primary, ChatModel secondary) { final primaryFinal = primary.status == MessageStatus.sent || primary.status == MessageStatus.received; final secondaryFinal = secondary.status == MessageStatus.sent || secondary.status == MessageStatus.received; if (primaryFinal != secondaryFinal) { return primaryFinal ? primary.status : secondary.status; } if (primary.status == MessageStatus.failed && secondary.status != MessageStatus.failed) { return secondary.status; } if (secondary.status == MessageStatus.failed && primary.status != MessageStatus.failed) { return primary.status; } if (primary.status == MessageStatus.sending && secondary.status != MessageStatus.sending) { return secondary.status; } return primary.status; } ChatModel _mergeModels( ChatModel existing, ChatModel candidate, { bool preferCandidate = false, }) { final candidateQuality = _messageQuality(candidate); final existingQuality = _messageQuality(existing); final useCandidateAsPrimary = preferCandidate || candidateQuality > existingQuality || (candidateQuality == existingQuality && _compareMessages(candidate, existing) < 0); final primary = useCandidateAsPrimary ? candidate : existing; final secondary = useCandidateAsPrimary ? existing : candidate; final merged = ChatModel( id: primary.id ?? secondary.id, localId: primary.localId ?? secondary.localId, body: _pickPreferredBody(primary.body, secondary.body), rawBody: _pickPreferredString(primary.rawBody, secondary.rawBody), rawViewBody: _pickPreferredString(primary.rawViewBody, secondary.rawViewBody), encryptedPayload: _pickPreferredString( primary.encryptedPayload, secondary.encryptedPayload), statusLabel: _pickPreferredString(primary.statusLabel, secondary.statusLabel), packetId: _pickPreferredString(primary.packetId, secondary.packetId), packetMode: _pickPreferredString(primary.packetMode, secondary.packetMode), rawPacketId: _pickPreferredString(primary.rawPacketId, secondary.rawPacketId), date: primary.date == 0 ? secondary.date : (secondary.date == 0 ? primary.date : min(primary.date, secondary.date)), isMe: primary.isMe, status: _pickPreferredStatus(primary, secondary), isSecure: primary.isSecure || secondary.isSecure || (primary.packetMode ?? secondary.packetMode) != null || (primary.encryptedPayload ?? secondary.encryptedPayload) != null, canRetryDecryption: primary.canRetryDecryption || secondary.canRetryDecryption, isPendingMultipart: primary.isPendingMultipart || secondary.isPendingMultipart, isRead: primary.isRead && secondary.isRead, ); if (!_isProgressBody(merged.body)) { merged.isPendingMultipart = false; if (merged.statusLabel != null && (merged.status == MessageStatus.sent || merged.status == MessageStatus.received) && !_isSecurePlaceholderBody(merged.body)) { merged.statusLabel = null; } } if (merged.isSecure || merged.packetMode != null || (merged.rawBody?.isNotEmpty ?? false)) { _enrichSecureModel(merged); } else { merged.rawViewBody ??= _buildRawViewBody( body: merged.body, rawBody: merged.rawBody, encryptedPayload: merged.encryptedPayload, packetMode: merged.packetMode, isSecure: merged.isSecure, ); } return merged; } List _reconcileCandidates(List<_MessageCandidate> candidates) { final mergedMessages = []; final aliasSets = >[]; for (final entry in candidates) { final prepared = _cloneMessage(entry.message); if (prepared.isSecure || prepared.packetMode != null || (prepared.rawBody?.isNotEmpty ?? false)) { _enrichSecureModel(prepared); } else { prepared.rawViewBody ??= _buildRawViewBody( body: prepared.body, rawBody: prepared.rawBody, encryptedPayload: prepared.encryptedPayload, packetMode: prepared.packetMode, isSecure: prepared.isSecure, ); } final workingAliases = _messageAliases(prepared).toSet(); final matches = []; for (var i = 0; i < aliasSets.length; i++) { if (_hasAliasOverlap(aliasSets[i], workingAliases)) { matches.add(i); } } if (matches.isEmpty) { mergedMessages.add(prepared); aliasSets.add( workingAliases.isEmpty ? {_fallbackMessageKey(prepared)} : workingAliases, ); continue; } var combined = prepared; for (final index in matches.reversed) { combined = _mergeModels( mergedMessages[index], combined, preferCandidate: entry.preferCandidate, ); mergedMessages.removeAt(index); aliasSets.removeAt(index); } final combinedAliases = _messageAliases(combined).toSet() ..addAll(workingAliases); mergedMessages.add(combined); aliasSets.add( combinedAliases.isEmpty ? {_fallbackMessageKey(combined)} : combinedAliases, ); } mergedMessages.sort(_compareMessages); return mergedMessages; } void _replaceMessagesWithCandidates(List<_MessageCandidate> candidates) { _publishMessages(_reconcileCandidates(candidates)); } String? _findOutgoingDraftLocalId(ChatModel candidate) { if (!candidate.isMe) return null; ChatModel? best; var bestDelta = 1 << 30; for (final message in messages) { if (message.localId == null || !message.isMe || message.id != null || (message.status != MessageStatus.sending && message.status != MessageStatus.failed && !message.isPendingMultipart)) { continue; } if (candidate.packetMode != null && message.packetMode != null && candidate.packetMode != message.packetMode) { continue; } final delta = (candidate.date - message.date).abs(); if (delta > 30000 || delta >= bestDelta) continue; best = message; bestDelta = delta; } return best?.localId; } bool _shouldKeepTransientMessageDuringReload(ChatModel message) { if (message.id != null) return false; final ageMs = DateTime.now().millisecondsSinceEpoch - message.date; if (ageMs <= _liveMessageGraceMs) return true; if (message.status == MessageStatus.failed || message.status == MessageStatus.sending || message.isPendingMultipart) { return true; } return message.isMe; } bool _shouldPreferInMemoryMessage(ChatModel message) { return message.id == null && ((DateTime.now().millisecondsSinceEpoch - message.date) <= _liveMessageGraceMs || message.status == MessageStatus.failed || message.status == MessageStatus.sending || message.isPendingMultipart || message.isMe); } String? _buildRawViewBody({ required String body, String? rawBody, String? encryptedPayload, String? packetMode, bool isSecure = false, }) { // If it's a secure message, we want to show the underlying protocol string if (encryptedPayload != null && encryptedPayload.isNotEmpty) { switch (packetMode) { case 'AE': return '@S:AE|$encryptedPayload'; case 'SYM': default: return '@S:SYM|$encryptedPayload'; } } // Fallback to the original raw SMS body if available if (rawBody != null && rawBody.isNotEmpty && rawBody != body) { return rawBody; } return null; } void _debouncedLoadMessages() { _debounceTimer?.cancel(); _debounceTimer = Timer(const Duration(milliseconds: 400), () { if (mounted) loadMessages(); }); } @override void initState() { super.initState(); displayName = ContactHelper.getName(widget.address); SecureMessagingService.instance.currentChatPhone = ContactHelper.normalizePhone(widget.address); _scrollController.addListener(_onScroll); _fetchSimCards(); _initMessageStreamListener(); _syncAndLoad(); DatabaseHelper.instance.markAsRead(widget.address); } void _onScroll() { if (!_scrollController.hasClients) return; final pos = _scrollController.position; if (pos.pixels >= pos.maxScrollExtent - 150 && !_isLoading && _hasMore) { loadMessages(isLoadMore: true); } } @override void dispose() { if (SecureMessagingService.instance.currentChatPhone == ContactHelper.normalizePhone(widget.address)) { SecureMessagingService.instance.currentChatPhone = null; } _scrollController.dispose(); _messageNotifier.dispose(); _messageSubscription?.cancel(); _msgController.dispose(); _debounceTimer?.cancel(); super.dispose(); } void _initMessageStreamListener() { _messageSubscription = SecureMessagingService.instance.messageStream.listen((data) { final normalizedTarget = ContactHelper.normalizePhone(widget.address); final normalizedIncoming = ContactHelper.normalizePhone(data['phone'] as String); if (normalizedIncoming == normalizedTarget) { // Immediate UI feedback for security state final result = data['result'] as Map?; if (result != null) { final isSecure = result['isSecure'] == true; final body = data['body'] as String?; if (isSecure && body != null) { // Trigger a load immediately to reflect state changes } } final originalBody = data['originalBody'] as String? ?? ''; final body = data['body'] as String? ?? ''; final packetId = data['packetId'] as String?; final packetMode = data['packetMode'] as String?; final eventDate = data['date'] as int? ?? DateTime.now().millisecondsSinceEpoch; final isMultipartPending = data['isMultipartPending'] as bool? ?? false; final isMultipartComplete = data['isMultipartComplete'] as bool? ?? false; final receivedParts = data['receivedParts'] as int? ?? 0; final totalPartsFromEvent = data['totalParts'] as int? ?? 0; final isMeEvent = data['isMe'] as bool? ?? false; if (body == 'REFRESH') { _debouncedLoadMessages(); return; } if (ProtocolHelper.parseMessage(originalBody)['type'] == 'plain' && _looksLikeProtocolFragment(originalBody)) { _debouncedLoadMessages(); return; } final bool isIntermediateFragment = body.contains('در حال دریافت قطعات') || body.contains('در حال ارسال قطعات') || body.contains('باقی قطعات پیام نرسیده‌اند'); if (isIntermediateFragment) { if (!mounted) return; if (isMultipartPending && packetId != null) { final liveBody = body; final liveStatus = totalPartsFromEvent > 0 && receivedParts > 0 ? '${isMeEvent ? 'ارسال' : 'دریافت'} زنده: $receivedParts از $totalPartsFromEvent قطعه' : '${isMeEvent ? 'ارسال' : 'دریافت'} قطعات در حال انجام است'; final pendingMessage = ChatModel( localId: isMeEvent ? _findOutgoingDraftLocalId(ChatModel( body: liveBody, date: eventDate, isMe: true, status: MessageStatus.sending, )) : null, body: liveBody, rawBody: originalBody, rawViewBody: _buildRawViewBody( body: liveBody, rawBody: originalBody, packetMode: packetMode, isSecure: true, ), packetId: packetId, packetMode: packetMode, statusLabel: liveStatus, date: eventDate, isMe: isMeEvent, status: isMeEvent ? MessageStatus.sending : MessageStatus.received, isSecure: true, isPendingMultipart: true, ); _replaceMessagesWithCandidates([ ...messages.map((m) => _MessageCandidate(m)), _MessageCandidate(pendingMessage, preferCandidate: true), ]); } return; } if (!mounted) return; final payloadMarker = _extractPayloadFromProcessedBody(body); final cleanBody = payloadMarker != null ? body.substring(0, body.indexOf(' ::PAYLOAD::')) : body; final incomingMessage = ChatModel( body: cleanBody, rawBody: originalBody, rawViewBody: _buildRawViewBody( body: cleanBody, rawBody: originalBody, encryptedPayload: payloadMarker, packetMode: packetMode, isSecure: isMultipartComplete || ProtocolHelper.parseMessage(originalBody)['type'] != 'plain', ), encryptedPayload: payloadMarker, packetId: packetId, packetMode: packetMode, date: eventDate, isMe: isMeEvent, status: isMeEvent ? MessageStatus.sent : MessageStatus.received, isSecure: isMultipartComplete || ProtocolHelper.parseMessage(originalBody)['type'] != 'plain', canRetryDecryption: payloadMarker != null, isPendingMultipart: false, statusLabel: payloadMarker != null ? 'پیام کامل شد؛ برای بازگشایی کلید را وارد کنید' : null, ); if (incomingMessage.isSecure || packetMode != null) { _enrichSecureModel(incomingMessage); } incomingMessage.localId ??= _findOutgoingDraftLocalId(incomingMessage); _replaceMessagesWithCandidates([ ...messages.map((m) => _MessageCandidate(m)), _MessageCandidate(incomingMessage, preferCandidate: true), ]); } }); } 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 _isSecurePlaceholderBody(String body) { if (body.contains(' ::PAYLOAD::')) return true; return body.startsWith('پیام امن') || body.startsWith('بازگشایی') || body.startsWith('پیام رمزنگاری غیر متقارن'); } bool _looksLikeProtocolFragment(String body) { final normalized = body.trimLeft().toUpperCase(); return normalized.startsWith('@S:AF|') || normalized.startsWith('@S:SFRA|') || normalized.startsWith('@G:AF|') || normalized.startsWith('@G:SFRA|'); } void _enrichSecureModel(ChatModel msg) { final originalBody = msg.body; final payloadFromProcessed = _extractPayloadFromProcessedBody(originalBody); if (payloadFromProcessed != null) { msg.body = originalBody.substring(0, originalBody.indexOf(' ::PAYLOAD::')); } final protocolParsed = ProtocolHelper.parseMessage(msg.rawBody ?? originalBody); final protocolPayload = protocolParsed['payload'] as String?; final protocolPacketId = protocolParsed['packetId'] as String?; final protocolType = protocolParsed['type'] as String?; msg.encryptedPayload = payloadFromProcessed ?? msg.encryptedPayload ?? (protocolPayload?.trim().isNotEmpty == true ? protocolPayload!.trim() : null); if ((msg.packetId == null || msg.packetId!.isEmpty) && protocolPacketId != null && protocolPacketId.isNotEmpty) { msg.packetId = protocolPacketId; } if ((msg.packetMode == null || msg.packetMode!.isEmpty) && protocolType != null) { if (protocolType == 'sym' || protocolType == 'sfra') { msg.packetMode = 'SYM'; } else if (protocolType == 'asym' || protocolType == 'afrag') { msg.packetMode = 'AE'; } } msg.rawViewBody = _buildRawViewBody( body: msg.body, rawBody: msg.rawBody, encryptedPayload: msg.encryptedPayload, packetMode: msg.packetMode, isSecure: true, ); msg.canRetryDecryption = !msg.isMe && _isSecurePlaceholderBody(msg.body); } String? _extractPayloadFromProcessedBody(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; } String _securityLabel(String level) { switch (level) { case 'symmetric': return 'رمزنگاری متقارن'; case 'asymmetric': return asymmetricModeLabel; default: return 'ارسال عادی'; } } String _securityDescription(String level) { switch (level) { case 'symmetric': return 'سریع‌تر برای گفتگوهای مشترک با کلید ثابت'; case 'asymmetric': return 'امن‌تر برای تبادل کلید و پیام‌های حساس'; default: return 'پیامک معمولی بدون لایه امنیتی اضافه'; } } IconData _securityIcon(String level) { switch (level) { case 'symmetric': return Icons.key_rounded; case 'asymmetric': return Icons.verified_user_rounded; default: return Icons.chat_bubble_outline_rounded; } } Color _securityAccent(String level) { switch (level) { case 'symmetric': return const Color(0xFF2E7D32); case 'asymmetric': return const Color(0xFF1565C0); default: return const Color(0xFF607D8B); } } ({String text, Color color}) _securityModeBadge(String level) { final secureState = contactInfo?['secure_state'] as String? ?? 'none'; final verification = contactInfo?['verification_state'] as String? ?? 'unverified'; final hasPeerKey = (contactInfo?['peer_public_key'] as String?)?.isNotEmpty == true; final hasSharedKey = (contactInfo?['ecc_shared_key'] as String?)?.isNotEmpty == true; final hasSymmetricKey = (contactInfo?['symmetric_key'] as String?)?.isNotEmpty == true; switch (level) { case 'symmetric': return hasSymmetricKey ? (text: 'آماده', color: const Color(0xFF2E7D32)) : (text: 'نیاز به کلید', color: const Color(0xFFEF6C00)); case 'asymmetric': if (secureState == 'handshake_failed' || verification == 'changed') { return (text: 'نیاز به بازیابی', color: const Color(0xFFC62828)); } if (secureState == 'handshake_pending') { return (text: 'در حال تبادل', color: const Color(0xFF1565C0)); } if (hasPeerKey && hasSharedKey && verification == 'verified' && secureState == 'ready') { return (text: 'آماده', color: const Color(0xFF2E7D32)); } if (hasPeerKey && hasSharedKey) { return (text: 'نیاز به تایید', color: const Color(0xFFEF6C00)); } return (text: 'نیاز به ایجاد', color: const Color(0xFFEF6C00)); default: return (text: 'همیشه آماده', color: const Color(0xFF607D8B)); } } Future _showSecurityModeSheet() async { final selected = await showModalBottomSheet( context: context, backgroundColor: Colors.transparent, builder: (context) { final levels = ['normal', 'symmetric', 'asymmetric']; return Container( decoration: BoxDecoration( color: AppTheme.darkCard, borderRadius: const BorderRadius.vertical(top: Radius.circular(28)), border: Border.all( color: Colors.white.withValues(alpha: 0.1), width: 0.5), ), padding: const EdgeInsets.fromLTRB(18, 18, 18, 24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 42, height: 4, decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(999), ), ), ), const SizedBox(height: 16), const Text( 'روش رمزنگاری', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white), ), const SizedBox(height: 6), const Text( 'برای این پیام، حالت مناسب را انتخاب کنید.', style: TextStyle(color: Colors.white60, height: 1.4), ), const SizedBox(height: 16), ...levels.map((level) { final isSelected = _selectedSecurityLevel == level; final accent = _securityAccent(level); final badge = _securityModeBadge(level); return Padding( padding: const EdgeInsets.only(bottom: 10), child: InkWell( borderRadius: BorderRadius.circular(18), onTap: () => Navigator.of(context).pop(level), child: AnimatedContainer( duration: const Duration(milliseconds: 180), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: isSelected ? accent.withValues(alpha: 0.15) : Colors.white.withValues(alpha: 0.03), borderRadius: BorderRadius.circular(18), border: Border.all( color: isSelected ? accent.withValues(alpha: 0.6) : Colors.white.withValues(alpha: 0.1), width: isSelected ? 1.6 : 1, ), ), child: Row( children: [ Container( width: 42, height: 42, decoration: BoxDecoration( color: accent.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(14), ), child: Icon(_securityIcon(level), color: accent), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( _securityLabel(level), style: const TextStyle( fontWeight: FontWeight.w700, color: Colors.white, ), ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: badge.color.withValues(alpha: 0.10), borderRadius: BorderRadius.circular(999), ), child: Text( badge.text, style: TextStyle( color: badge.color, fontSize: 11, fontWeight: FontWeight.w700, ), ), ), ], ), const SizedBox(height: 4), Text( _securityDescription(level), style: const TextStyle( color: Colors.white38, fontSize: 12), ), ], ), ), const SizedBox(width: 8), Icon( isSelected ? Icons.radio_button_checked_rounded : Icons.radio_button_off_rounded, color: isSelected ? accent : const Color(0xFF97A6B5), ), ], ), ), ), ); }), ], ), ); }, ); if (selected != null && mounted) { setState(() => _selectedSecurityLevel = selected); } } String _securityStatusText() { final state = contactInfo?['secure_state'] as String? ?? 'none'; final verification = contactInfo?['verification_state'] as String? ?? 'unverified'; final mode = contactInfo?['mode'] as String? ?? 'normal'; if (state == 'handshake_pending') { return 'در حال تبادل کلید عمومی...'; } if (state == 'handshake_failed') { return 'تبادل کلید ناموفق بود؛ دوباره تلاش کنید'; } if (mode == 'asymmetric' && (contactInfo?['peer_public_key'] as String?)?.isNotEmpty == true) { if (verification == 'changed') { return 'کلید مخاطب تغییر کرده است؛ اثر انگشت را دوباره تطبیق دهید'; } return verification == 'verified' && state == 'ready' ? 'ارتباط امن شد و کانال ECC آماده است' : 'کلید عمومی دریافت شده؛ برای فعال شدن ECC اثر انگشت را تایید کنید'; } if (mode == 'symmetric' && (contactInfo?['symmetric_key'] as String?)?.isNotEmpty == true) { return 'رمزنگاری متقارن آماده است'; } return 'حالت عادی'; } bool get _isAsymmetricReadyNow { final mode = contactInfo?['mode'] as String? ?? 'normal'; final secureState = contactInfo?['secure_state'] as String? ?? 'none'; final verification = contactInfo?['verification_state'] as String? ?? 'unverified'; final hasPeerKey = (contactInfo?['peer_public_key'] as String?)?.isNotEmpty == true; final hasSharedKey = (contactInfo?['ecc_shared_key'] as String?)?.isNotEmpty == true; return mode == 'asymmetric' && hasPeerKey && hasSharedKey && secureState == 'ready' && verification == 'verified'; } Future _startAsymmetricHandshake({bool recovery = false}) async { final result = await SecureMessagingService.instance .startAsymmetricHandshake(widget.address); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( recovery ? 'بازیابی ارتباط امن شروع شد. ${result['notice']}' : (result['notice'] as String), ), )); await loadMessages(); } Future _endAsymmetricSecurity() async { await SecureMessagingService.instance .resetAsymmetricSecurity(widget.address); if (!mounted) return; setState(() { _dismissedBannerText = null; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('ارتباط امن برای این مخاطب پایان یافت.'), ), ); await loadMessages(); } Widget _buildAsymmetricActions() { if (_selectedSecurityLevel != 'asymmetric') { return const SizedBox.shrink(); } final secureState = contactInfo?['secure_state'] as String? ?? 'none'; final verification = contactInfo?['verification_state'] as String? ?? 'unverified'; final hasPeerKey = (contactInfo?['peer_public_key'] as String?)?.isNotEmpty == true; final hasSharedKey = (contactInfo?['ecc_shared_key'] as String?)?.isNotEmpty == true; final needsVerification = hasPeerKey && hasSharedKey && verification != 'verified' && secureState != 'handshake_pending'; final needsCreate = !needsVerification && !_isAsymmetricReadyNow && secureState != 'handshake_pending' && verification != 'changed'; final canRecover = _isAsymmetricReadyNow || secureState == 'handshake_failed' || secureState == 'pending_verification' || verification == 'changed'; final canEnd = secureState == 'handshake_pending' || secureState == 'handshake_failed' || secureState == 'pending_verification' || (contactInfo?['mode'] as String? ?? 'normal') == 'asymmetric' || hasPeerKey; return Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: AppTheme.glassWrapper( child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _isAsymmetricReadyNow ? 'ارتباط امن تایید شده است. حالا می‌توانید پیام‌های ECC بفرستید.' : secureState == 'handshake_pending' ? 'در حال تبادل کلیدهای عمومی هستیم. بعد از دریافت پاسخ، اثر انگشت مخاطب را تایید کنید.' : verification == 'changed' ? 'کلید مخاطب تغییر کرده است. تا زمان تایید اثر انگشت جدید، ارسال ECC غیرفعال می‌ماند.' : needsVerification ? 'کلید عمومی مخاطب دریافت شده است. تا زمان تایید اثر انگشت، ارسال ECC غیرفعال می‌ماند.' : 'برای شروع ارتباط نامتقارن، ابتدا تبادل کلید را انجام دهید.', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white), ), const SizedBox(height: 14), Wrap( spacing: 8, runSpacing: 8, children: [ if (needsCreate) ElevatedButton.icon( onPressed: () => _startAsymmetricHandshake(), style: ElevatedButton.styleFrom( backgroundColor: Colors.blueAccent.withValues(alpha: 0.8), foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), ), icon: const Icon(Icons.security, size: 18), label: const Text('ایجاد ارتباط امن'), ), if (secureState == 'handshake_pending') OutlinedButton.icon( onPressed: null, style: OutlinedButton.styleFrom( foregroundColor: Colors.white60, side: const BorderSide(color: Colors.white24), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), ), icon: const SizedBox( width: 14, height: 14, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white60), ), label: const Text('در حال تبادل کلید'), ), if (hasPeerKey) OutlinedButton.icon( onPressed: _showFingerprintSheet, style: OutlinedButton.styleFrom( foregroundColor: Colors.white, side: const BorderSide(color: Colors.white30), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), ), icon: const Icon(Icons.verified_user_outlined, size: 18), label: Text(verification == 'verified' ? 'اثر انگشت' : 'تایید اثر انگشت'), ), if (canRecover) OutlinedButton.icon( onPressed: () => _startAsymmetricHandshake(recovery: true), style: OutlinedButton.styleFrom( foregroundColor: Colors.white, side: const BorderSide(color: Colors.white30), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), ), icon: const Icon(Icons.refresh_rounded, size: 18), label: const Text('بازیابی ارتباط امن'), ), if (canEnd) TextButton.icon( onPressed: _endAsymmetricSecurity, style: TextButton.styleFrom( foregroundColor: Colors.redAccent.withValues(alpha: 0.7)), icon: const Icon(Icons.cancel_outlined, size: 18), label: const Text('پایان ارتباط'), ), ], ), ], ), ), ), ); } Future _syncAndLoad() async { if (_isLoading) return; _isLoading = true; try { final inbox = await telephony.getInboxSms(columns: [ SmsColumn.ID, SmsColumn.ADDRESS, SmsColumn.BODY, SmsColumn.DATE, SmsColumn.TYPE ], filter: SmsFilter.where(SmsColumn.ADDRESS).equals(widget.address)); final sent = await telephony.getSentSms(columns: [ SmsColumn.ID, SmsColumn.ADDRESS, SmsColumn.BODY, SmsColumn.DATE, SmsColumn.TYPE ], filter: SmsFilter.where(SmsColumn.ADDRESS).equals(widget.address)); final List> toCache = []; for (final sms in [...inbox, ...sent]) { toCache.add({ 'sms_id': sms.id, 'address': ContactHelper.normalizePhone(sms.address ?? ''), 'body': sms.body ?? '', 'date': sms.date ?? 0, 'type': sms.type?.index ?? 0, 'is_me': sms.type != SmsType.MESSAGE_TYPE_INBOX ? 1 : 0, }); } if (toCache.isNotEmpty) { await DatabaseHelper.instance.batchInsertSms(toCache); } } catch (e) { debugPrint('Sync error: $e'); } finally { _isLoading = false; loadMessages(); } } bool _needsReload = false; Future loadMessages({bool isLoadMore = false}) async { if (_isLoading) { if (!isLoadMore) _needsReload = true; return; } if (isLoadMore && !_hasMore) return; _isLoading = true; _needsReload = false; if (!isLoadMore) { _hasMore = true; } try { final cInfo = await DatabaseHelper.instance.getContact(widget.address); final totalSmsCount = await DatabaseHelper.instance.getSmsCount( widget.address, ); if (mounted) setState(() => contactInfo = cInfo); final previousOffset = _offset; final fetchOffset = isLoadMore ? previousOffset : 0; final rawSms = await DatabaseHelper.instance .getPaginatedSms(widget.address, _limit, fetchOffset); // 1. Process Database SMS final List processed = await compute( (data) => MessageProcessor.processSmsList( data['raw'] as List>, data['address'] as String, ), {'raw': rawSms, 'address': widget.address}, ); // 2. Perform Unified Reconciliation final currentMessages = List.from(messages); final baseMessages = isLoadMore ? currentMessages : currentMessages .where(_shouldKeepTransientMessageDuringReload) .toList(growable: false); final candidates = <_MessageCandidate>[ ...baseMessages.map( (m) => _MessageCandidate( m, preferCandidate: _shouldPreferInMemoryMessage(m), ), ), ]; // Add Database Messages for (final msg in processed) { if (msg.isSecure) { final pBody = await SecureMessagingService.instance.processIncomingSms( widget.address, msg.body, broadcast: false, isMe: msg.isMe, isScan: false, ); if (pBody != null && pBody != msg.body) { msg.body = pBody; } _enrichSecureModel(msg); } candidates.add(_MessageCandidate(msg)); } // Merge Pending Fragments from Cache final pendingPacketRows = await SecureMessagingService.instance .getPendingPacketsForPhone(widget.address); for (final row in pendingPacketRows) { final pId = row['msg_id'] as String; final pMode = row['packet_mode'] as String? ?? 'SYM'; candidates.add( _MessageCandidate( ChatModel( body: 'در حال دریافت قطعات...', statusLabel: '(${row['received_parts']}/${row['total_parts']}) دریافت قطعات', packetId: pId, packetMode: pMode, date: row['first_seen'] as int? ?? 0, isMe: (row['isMe'] as int? ?? 0) == 1, status: MessageStatus.received, isSecure: true, isPendingMultipart: true, ), ), ); } // 3. Finalize List final List updatedList = _reconcileCandidates(candidates); final nextOffset = isLoadMore ? previousOffset + rawSms.length : max(previousOffset, rawSms.length); _offset = min(totalSmsCount, nextOffset); _hasMore = _offset < totalSmsCount; _publishMessages(updatedList); } finally { _isLoading = false; if (_needsReload) loadMessages(); } } Future _openContactInfo() async { if (!await FlutterContacts.requestPermission(readonly: true) || !mounted) { return; } showDialog( context: context, barrierDismissible: false, builder: (_) => const Center(child: CircularProgressIndicator()), ); try { final contacts = await FlutterContacts.getContacts( withProperties: true, withPhoto: false); final currentPhoneNormalized = ContactHelper.normalizePhone(widget.address); String? contactId; for (final contact in contacts) { for (final phone in contact.phones) { if (ContactHelper.normalizePhone(phone.number) == currentPhoneNormalized) { contactId = contact.id; break; } } if (contactId != null) break; } if (mounted) Navigator.pop(context); if (contactId != null) { await FlutterContacts.openExternalView(contactId); } else { await FlutterContacts.openExternalInsert( Contact(phones: [Phone(widget.address)])); } if (mounted) { setState(() { displayName = ContactHelper.getName(widget.address); }); } } catch (_) { if (mounted && Navigator.canPop(context)) Navigator.pop(context); } } Future _showFingerprintSheet() async { final securityContext = await SecureMessagingService.instance .getSecurityContext(widget.address); if (!mounted || securityContext == null) return; showModalBottomSheet( context: context, backgroundColor: Colors.transparent, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (context) { final peerFingerprint = securityContext['peerFingerprint'] as String?; final verificationState = securityContext['verificationState'] as String? ?? 'unverified'; final stateText = verificationState == 'changed' ? 'وضعیت: کلید مخاطب تغییر کرده است' : verificationState == 'verified' ? 'وضعیت: تایید شده' : 'وضعیت: هنوز تایید نشده'; final stateColor = verificationState == 'changed' ? Colors.red.shade700 : verificationState == 'verified' ? Colors.green.shade700 : Colors.orange.shade400; return Container( decoration: BoxDecoration( color: AppTheme.darkCard, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), border: Border.all( color: Colors.white.withValues(alpha: 0.1), width: 0.5), ), padding: const EdgeInsets.fromLTRB(20, 20, 20, 32), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text('تایید هویت و اثر انگشت کلید', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)), const SizedBox(height: 12), Text('کلید شما:\n${securityContext['localFingerprint']}', style: const TextStyle(color: Colors.white70)), const SizedBox(height: 12), Text( peerFingerprint == null ? 'هنوز کلید عمومی مخاطب دریافت نشده است.' : 'کلید مخاطب:\n$peerFingerprint', style: const TextStyle(color: Colors.white70)), const SizedBox(height: 12), const Text( 'برای جلوگیری از MITM، این اثر انگشت را با تماس تلفنی یا یک کانال امن دیگر با مخاطب تطبیق دهید.', style: TextStyle(height: 1.5, color: Colors.white60), ), const SizedBox(height: 12), Text( stateText, style: TextStyle( color: stateColor, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 16), ElevatedButton( onPressed: peerFingerprint == null ? null : () async { await SecureMessagingService.instance .markFingerprintVerified(widget.address); if (!mounted) return; Navigator.of(this.context).pop(); ScaffoldMessenger.of(this.context).showSnackBar( const SnackBar( content: Text( 'اثر انگشت کلید برای این مخاطب تایید شد.')), ); loadMessages(); }, child: Text(verificationState == 'changed' ? 'تایید اثر انگشت جدید' : 'تایید اثر انگشت'), ), ], ), ); }, ); } void _deleteSmsNative(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); try { await nativeChannel .invokeMethod('deleteSms', {'id': messageId.toString()}); if (!mounted) return; messages.removeWhere((m) => m.id == messageId); _publishMessages(messages); Future.delayed(const Duration(seconds: 1), loadMessages); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('درخواست حذف ارسال شد'))); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('خطا: $e'))); } }, child: const Text('حذف'), ), ], ), ); } Future sendMessage() async { final text = _msgController.text.trim(); if (text.isEmpty) return; if (_selectedSecurityLevel == 'symmetric') { _showKeyEntryAndSend(text); return; } if (_selectedSecurityLevel == 'asymmetric' && !_isAsymmetricReadyNow) { final secureState = contactInfo?['secure_state'] as String? ?? 'none'; final verification = contactInfo?['verification_state'] as String? ?? 'unverified'; final hasPeerKey = (contactInfo?['peer_public_key'] as String?)?.isNotEmpty == true; final hasSharedKey = (contactInfo?['ecc_shared_key'] as String?)?.isNotEmpty == true; final notice = verification == 'changed' ? 'کلید مخاطب تغییر کرده است؛ ابتدا اثر انگشت جدید را تایید کنید.' : (hasPeerKey && hasSharedKey) ? 'کلید مخاطب دریافت شده اما هنوز تایید نشده است؛ ابتدا اثر انگشت را تایید کنید.' : secureState == 'handshake_pending' ? 'تبادل کلید هنوز کامل نشده است.' : 'ابتدا روی «ایجاد ارتباط امن» بزنید تا تبادل کلید انجام شود.'; if (!mounted) return; ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(notice))); return; } await _executeSendMessage(text, securityLevel: _selectedSecurityLevel); } void _showKeyEntryAndSend(String text) { final keyController = TextEditingController(text: contactInfo?['symmetric_key'] ?? ''); showDialog( context: context, builder: (ctx) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), title: const Text('رمزنگاری متقارن'), content: Column( mainAxisSize: MainAxisSize.min, children: [ const Text('کلید متقارن این پیام را وارد یا تایید کنید:'), const SizedBox(height: 10), TextField( controller: keyController, decoration: const InputDecoration( hintText: 'کلید متقارن', border: OutlineInputBorder(), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('لغو')), ElevatedButton( onPressed: () { if (keyController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('لطفاً کلید را وارد کنید'))); return; } Navigator.pop(ctx); _executeSendMessage(text, securityLevel: 'symmetric', symmetricKey: keyController.text.trim()); }, child: const Text('ارسال'), ), ], ), ); } Future _executeSendMessage(String text, {required String securityLevel, String? symmetricKey}) async { final earlyPacketId = Random().nextInt(0xFFFFFFFF).toRadixString(16).padLeft(10, '0'); final tempTimestamp = DateTime.now().millisecondsSinceEpoch; final draftLocalId = 'draft::$tempTimestamp::${Random().nextInt(1 << 32).toRadixString(16)}'; final tempMsg = ChatModel( localId: draftLocalId, body: text, date: tempTimestamp, isMe: true, status: MessageStatus.sending, isSecure: securityLevel != 'normal', packetId: securityLevel == 'normal' ? null : earlyPacketId, ); messages.insert(0, tempMsg); _publishMessages(messages); _msgController.clear(); try { if (symmetricKey != null) { tempMsg.rawBody = await SecureMessagingService.instance .getEncryptedPreview(text, symmetricKey); } final result = await SecureMessagingService.instance.sendMessage( widget.address, text, securityLevel: securityLevel, symmetricKey: symmetricKey, forcedPacketId: securityLevel == 'normal' ? null : earlyPacketId, localTimestamp: tempTimestamp, ); if (!mounted) return; if (result['sentText'] == true) { tempMsg.packetId = result['packetId'] as String?; tempMsg.packetMode = result['packetMode'] as String?; tempMsg.rawBody ??= result['protocolBody'] as String?; tempMsg.rawViewBody ??= tempMsg.rawBody; tempMsg.encryptedPayload ??= result['encryptedPayload'] as String?; tempMsg.isPendingMultipart = result['notice'] == 'sent_fragmented'; tempMsg.status = MessageStatus.sent; if (result['notice'] == 'sent') { tempMsg.statusLabel = null; } else { tempMsg.statusLabel = 'ارسال کامل شد'; } _replaceMessagesWithCandidates([ ...messages.map((m) => _MessageCandidate(m)), _MessageCandidate(tempMsg, preferCandidate: true), ]); Future.delayed(const Duration(milliseconds: 800), loadMessages); } else { messages.remove(tempMsg); _msgController.text = text; _msgController.selection = TextSelection.fromPosition( TextPosition(offset: _msgController.text.length)); _publishMessages(messages); ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(result['notice'] as String))); loadMessages(); } } catch (e) { if (!mounted) return; setState(() { tempMsg.status = MessageStatus.failed; tempMsg.statusLabel = 'خطا در ارسال'; }); ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('خطا در ارسال: $e'))); } } Future _handleRetryDecryption(ChatModel msg) async { final source = msg.rawBody ?? msg.body; final payload = await SecureMessagingService.instance.resolvePayloadForRetry( source, phone: widget.address, hintedPayload: msg.body.contains(' ::PAYLOAD::') ? msg.body.split(' ::PAYLOAD::')[1] : msg.encryptedPayload, ); if (!mounted) return; if (payload == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('payload پیام پیدا نشد.'))); return; } final keyController = TextEditingController(); 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; Navigator.pop(ctx); final messenger = ScaffoldMessenger.of(context); final decrypted = await SecureMessagingService.instance.decryptWithKey( source, keyController.text.trim(), phone: widget.address, hintedPayload: payload, ); if (!mounted) return; if (decrypted != null) { final isGroup = msg.rawBody?.contains('@G:') == true || msg.body.contains('@G:') == true || (msg.rawBody != null && ProtocolHelper.parseMessage(msg.rawBody!)['isGroup'] == true); if (isGroup) { final groups = await DatabaseHelper.instance .getGroupsContainingPhone(widget.address); for (final g in groups) { await DatabaseHelper.instance.updateGroupKey( g['id'] as int, keyController.text.trim()); } } else { await DatabaseHelper.instance.updateContactSecurity( widget.address, symmetricKey: keyController.text.trim(), mode: 'symmetric', state: 'ready', ); } setState(() { msg.body = decrypted; msg.encryptedPayload = payload; msg.rawViewBody = _buildRawViewBody( body: decrypted, rawBody: msg.rawBody, encryptedPayload: payload, packetMode: msg.packetMode ?? 'SYM', isSecure: true, ); msg.canRetryDecryption = false; msg.statusLabel = null; msg.isSecure = true; msg.isPendingMultipart = false; }); messenger.showSnackBar( const SnackBar(content: Text('بازگشایی موفقیت‌آمیز بود.'))); loadMessages(); } else { messenger.showSnackBar(const SnackBar( content: Text( 'بازگشایی ناموفق بود. کلید اشتباه است یا دیتا مخدوش شده است.'))); } }, child: const Text('بازگشایی'), ), ], ), ); } Future _makePhoneCall() async { try { await nativeChannel.invokeMethod('makeCall', {'phone': widget.address}); } catch (_) { if (!mounted) return; ScaffoldMessenger.of(context) .showSnackBar(const SnackBar(content: Text('خطا در برقراری تماس'))); } } Widget _buildSecurityBanner() { final status = _securityStatusText(); if (status.isEmpty || status == _dismissedBannerText) { return const SizedBox.shrink(); } final mode = contactInfo?['mode'] as String? ?? 'normal'; final secureState = contactInfo?['secure_state'] as String? ?? 'none'; final verification = contactInfo?['verification_state'] as String? ?? 'unverified'; final isAsym = mode == 'asymmetric' || _selectedSecurityLevel == 'asymmetric'; final hasSecurityAlert = secureState == 'handshake_failed' || verification == 'changed'; final hasSecureConnection = isAsym && _isAsymmetricReadyNow; final bg = hasSecurityAlert ? Colors.red.withValues(alpha: 0.8) : (hasSecureConnection ? Colors.green.withValues(alpha: 0.8) : Colors.blue.withValues(alpha: 0.8)); final border = Colors.white.withValues(alpha: 0.2); const iconColor = Colors.white; const textColor = Colors.white; return AnimatedSwitcher( duration: const Duration(milliseconds: 400), transitionBuilder: (Widget child, Animation animation) { return FadeTransition( opacity: animation, child: SlideTransition( position: Tween( begin: const Offset(0, -0.5), end: Offset.zero, ).animate(animation), child: child, ), ); }, child: Container( key: ValueKey(status), margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: ClipRRect( borderRadius: BorderRadius.circular(16), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: bg.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(16), border: Border.all(color: border, width: 0.5), ), child: Row( children: [ Icon( hasSecurityAlert ? Icons.warning_amber_rounded : (hasSecureConnection ? Icons.verified_user_rounded : Icons.info_outline_rounded), color: iconColor, size: 20, ), const SizedBox(width: 12), Expanded( child: Text( status, style: const TextStyle( color: textColor, fontSize: 13, fontWeight: FontWeight.w600, ), ), ), IconButton( onPressed: () => setState(() => _dismissedBannerText = status), icon: const Icon(Icons.close, color: Colors.white60, size: 18), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), ), ), ), ), ); } @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: _openContactInfo, overlayColor: WidgetStateProperty.all(Colors.transparent), child: Row( mainAxisSize: MainAxisSize.min, children: [ Hero( tag: 'avatar_${displayName}_ind', child: Container( width: 38, height: 38, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( colors: [ Theme.of(context).primaryColor, Theme.of(context) .primaryColor .withValues(alpha: 0.7), ], ), ), child: Center( child: Text( displayName.isNotEmpty ? displayName[0] : '?', style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold), ), ), ), ), const SizedBox(width: 10), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( displayName, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), ), if (displayName != widget.address) Text( widget.address, style: const TextStyle( fontSize: 11, color: Colors.white70), ), ], ), ], ), ), actions: [ IconButton( icon: const Icon(Icons.verified_user_outlined, color: Colors.white, size: 22), onPressed: _showFingerprintSheet, ), IconButton( icon: const Icon(Icons.call_outlined, color: Colors.white, size: 22), onPressed: _makePhoneCall, ), ], ), ), ), body: Stack( children: [ Positioned.fill(child: CustomPaint(painter: MeshBackgroundPainter())), Column( children: [ SizedBox( height: MediaQuery.of(context).padding.top + kToolbarHeight + 8), _buildSecurityBanner(), Expanded( child: ValueListenableBuilder>( valueListenable: _messageNotifier, builder: (context, messages, child) { if (messages.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.mark_chat_unread_outlined, size: 60, color: Colors.white.withValues(alpha: 0.1)), const SizedBox(height: 10), const Text('آغاز گفتگو...', style: TextStyle( color: Colors.white38, fontSize: 13)), ], ), ); } return ListView.builder( key: const PageStorageKey('chat_list_view'), controller: _scrollController, reverse: true, padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 20), itemCount: messages.length + (_hasMore ? 1 : 0), itemBuilder: (context, index) { if (index == messages.length) { return const Center( child: Padding( padding: EdgeInsets.all(12.0), child: CircularProgressIndicator(strokeWidth: 2), ), ); } final msg = messages[index]; return GestureDetector( onLongPress: () { if (msg.id != null) { _deleteSmsNative(msg.id!); } }, child: MessageBubble( key: ValueKey( msg.localId ?? _packetKey(msg.packetId, msg.packetMode) ?? msg.id?.toString() ?? 'temp_${msg.date}', ), body: msg.body, rawBody: msg.rawViewBody ?? msg.rawBody, statusLabel: msg.statusLabel, 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(12, 8, 12, 12), 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, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!_loadingSims && _simCards.length > 1) SizedBox( height: 35, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: _simCards.length, itemBuilder: (context, index) { final sim = _simCards[index]; final isSelected = _selectedSim == sim; return Padding( padding: const EdgeInsets.only(right: 8), child: ChoiceChip( label: Text(sim['displayName']?.toString() ?? 'SIM ${index + 1}'), selected: isSelected, onSelected: (_) => setState(() => _selectedSim = sim), ), ); }, ), ), const SizedBox(height: 8), Row( children: [ InkWell( borderRadius: BorderRadius.circular(18), onTap: _showSecurityModeSheet, child: Container( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 12), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(18), border: Border.all( color: Colors.white.withValues(alpha: 0.1), width: 0.5), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: _securityAccent( _selectedSecurityLevel) .withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10), ), child: Icon( _securityIcon(_selectedSecurityLevel), size: 16, color: _securityAccent( _selectedSecurityLevel), ), ), const SizedBox(width: 10), Text( _securityLabel(_selectedSecurityLevel), style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, color: Colors.white), ), const SizedBox(width: 8), const Icon(Icons.expand_more_rounded, size: 18, color: Colors.white70), ], ), ), ), ], ), _buildAsymmetricActions(), const SizedBox(height: 10), 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: 10), FloatingActionButton( heroTag: 'send_${widget.address}', mini: true, elevation: 4, backgroundColor: Theme.of(context).primaryColor, onPressed: _selectedSecurityLevel == 'asymmetric' && !_isAsymmetricReadyNow ? null : sendMessage, child: const Icon(Icons.send_rounded, color: Colors.white, size: 20), ), ], ), ], ), ), ), ], ), ], ), ); } }