import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import '/data/models/message_model.dart'; import '/data/models/contact_model.dart'; import 'package:chepuhagram/presentation/widgets/message_bubble.dart'; import 'package:chepuhagram/data/repositories/contact_repository.dart'; import 'package:chepuhagram/domain/services/crypto_service.dart'; import 'package:chepuhagram/data/datasources/ws_client.dart'; import 'package:provider/provider.dart'; import '/logic/contact_provider.dart'; import '../../domain/services/api_service.dart'; import 'package:chepuhagram/data/datasources/local_db_service.dart'; import 'package:chepuhagram/main.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'contacts_screen.dart'; import 'package:flutter/services.dart'; import 'user_profile_screen.dart'; class ChatScreen extends StatefulWidget { final Contact contact; const ChatScreen({super.key, required this.contact}); @override State createState() => _ChatScreenState(); } class _ChatScreenState extends State { static const String _notificationLaunchKey = 'notification_launch_data'; int myId = 0; late Contact _currentContact; bool _isKeyLoading = false; final TextEditingController _controller = TextEditingController(); final FocusNode _inputFocusNode = FocusNode(); final ContactRepository _contactRepository = ContactRepository(); final apiService = ApiService(); final CryptoService _cryptoService = CryptoService(); List messages = []; StreamSubscription? _socketSubscription; final Set _sentReadReceipts = {}; final LocalDbService _localDbService = LocalDbService(); MessageModel? _replyTo; @override void initState() { super.initState(); _currentContact = widget.contact; currentActiveChatContactId = _currentContact.id; // Устанавливаем активный чат final contactProvider = context.read(); myId = contactProvider.getCurrentUserId() ?? 0; // Если ключа нет, загружаем его при входе if (_currentContact.publicKey == null) { _loadContactKey(); } _loadHistory(); final socketService = Provider.of(context, listen: false); _socketSubscription = socketService.messages.listen(_handleIncomingMessage); } Future _loadContactKey() async { if (!mounted) return; setState(() => _isKeyLoading = true); try { final updatedContact = await _contactRepository.fetchContactById( _currentContact.id, ); if (!mounted) return; setState(() { _currentContact = updatedContact; _isKeyLoading = false; }); print(updatedContact.publicKey); } catch (e) { if (!mounted) return; setState(() => _isKeyLoading = false); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Не удалось получить ключ шифрования собеседника"), ), ); } } @override void dispose() { currentActiveChatContactId = null; // Сбрасываем активный чат _socketSubscription?.cancel(); _controller.dispose(); _inputFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (_) => const ContactsScreen()), ); }, ), title: GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (_) => UserProfileScreen( userId: _currentContact.id, username: _currentContact.username, name: _currentContact.name, ), ), ); }, child: Text(_currentContact.name), ), ), body: Column( children: [ Expanded( child: ListView.builder( reverse: true, // Сообщения растут снизу вверх itemCount: messages.length, itemBuilder: (context, index) { final msg = messages[messages.length - 1 - index]; return MessageBubble( message: msg, onTap: () => _showMessageActions(msg), ); }, ), ), _buildMessageInput(), ], ), ); } Future _showMessageActions(MessageModel msg) async { if (!mounted) return; await showModalBottomSheet( context: context, showDragHandle: true, builder: (ctx) { return SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.reply), title: const Text('Ответить'), onTap: () { Navigator.of(ctx).pop(); setState(() => _replyTo = msg); _inputFocusNode.requestFocus(); }, ), ListTile( leading: const Icon(Icons.copy), title: const Text('Скопировать'), onTap: () async { Navigator.of(ctx).pop(); await Clipboard.setData(ClipboardData(text: msg.text)); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Скопировано')), ); }, ), ListTile( leading: const Icon(Icons.forward), title: const Text('Переслать'), onTap: () { Navigator.of(ctx).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Пересылка пока не реализована')), ); }, ), ListTile( leading: const Icon(Icons.delete_outline), title: const Text('Удалить'), textColor: Colors.red, iconColor: Colors.red, onTap: () async { Navigator.of(ctx).pop(); setState(() { messages.removeWhere( (m) => (m.id != null && m.id == msg.id) || (m.tempId != null && m.tempId == msg.tempId), ); }); final id = msg.id; if (id != null) { try { await _localDbService.deleteMessage(id); } catch (_) {} } }, ), const SizedBox(height: 8), ], ), ); }, ); } Widget _buildMessageInput() { return SafeArea( // Добавляем SafeArea здесь child: Container( color: Theme.of(context).colorScheme.surfaceVariant, padding: const EdgeInsets.all(8.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ if (_replyTo != null) Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(12), ), child: Row( children: [ const Icon(Icons.reply, size: 18), const SizedBox(width: 8), Expanded( child: Text( _replyTo!.text, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), IconButton( icon: const Icon(Icons.close, size: 18), onPressed: () => setState(() => _replyTo = null), ), ], ), ), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: TextField( controller: _controller, focusNode: _inputFocusNode, minLines: 1, maxLines: 5, textInputAction: TextInputAction.newline, decoration: const InputDecoration( hintText: "Напиши сообщение...", ), ), ), IconButton( icon: const Icon(Icons.send), onPressed: () { _sendMessage(); }, ), ], ), ], ), ), ); } Future _sendMessage() async { final rawText = _controller.text.trim(); if (rawText.isEmpty) return; _controller.clear(); if (_currentContact.publicKey == null) { await _loadContactKey(); if (_currentContact.publicKey == null) return; } try { final myPrivKey = await _cryptoService.getPrivateKey(); final sharedSecret = await _cryptoService.deriveSharedSecret( myPrivKey!, _currentContact.publicKey!, ); final encryptedText = await _cryptoService.encryptMessage( rawText, sharedSecret, ); final encryptedText50 = await _cryptoService.encryptMessage( rawText.length > 50 ? rawText.substring(0, 50) : rawText, sharedSecret, ); final tempId = DateTime.now().microsecondsSinceEpoch; final localMessage = MessageModel( tempId: tempId, text: rawText, isMe: true, senderId: myId, receiverId: _currentContact.id, createdAt: DateTime.now(), status: MessageStatus.sending, replyToId: _replyTo?.id, replyToText: _replyTo?.text, ); setState(() { messages.add(localMessage); }); // Формируем payload для сервера final payload = { "type": "private_message", "receiver_id": _currentContact.id, "content": encryptedText, "content50": encryptedText50, "temp_id": tempId, if (_replyTo?.id != null) ...{ "reply_to_id": _replyTo!.id, "reply_to_text": _replyTo!.text, }, }; // Отправляем print("ОТПРАВКА: $payload"); final ok = Provider.of(context, listen: false).sendMessage(payload); if (!mounted) return; setState(() { final idx = messages.indexWhere((m) => m.tempId == tempId); if (idx == -1) return; messages[idx] = messages[idx].copyWith( status: ok ? MessageStatus.sent : MessageStatus.failed, ); _replyTo = null; }); _controller.clear(); } catch (e) { _controller.text = rawText; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text("Ошибка шифрования: $e"))); } } void _handleIncomingMessage(Map data) async { // ACK от сервера: сообщение сохранено и получило server_id if (data['type'] == 'message_sent') { final tempId = int.tryParse(data['temp_id']?.toString() ?? ''); final serverId = int.tryParse(data['server_id']?.toString() ?? ''); final ts = DateTime.tryParse(data['timestamp']?.toString() ?? ''); if (tempId == null) return; if (!mounted) return; setState(() { final idx = messages.indexWhere((m) => m.tempId == tempId); if (idx == -1) return; messages[idx] = messages[idx].copyWith( id: serverId ?? messages[idx].id, createdAt: ts ?? messages[idx].createdAt, status: MessageStatus.sent, ); }); return; } // Backward compatibility: старый ack мог приходить как message_delivered с temp_id/server_id if (data['type'] == 'message_delivered' && data.containsKey('temp_id')) { final tempId = int.tryParse(data['temp_id']?.toString() ?? ''); final serverId = int.tryParse(data['server_id']?.toString() ?? ''); final ts = DateTime.tryParse(data['timestamp']?.toString() ?? ''); if (tempId == null) return; if (!mounted) return; setState(() { final idx = messages.indexWhere((m) => m.tempId == tempId); if (idx == -1) return; messages[idx] = messages[idx].copyWith( id: serverId ?? messages[idx].id, createdAt: ts ?? messages[idx].createdAt, status: MessageStatus.sent, ); }); return; } // Доставка онлайн (получатель был в сети) if (data['type'] == 'message_delivered') { final messageId = int.tryParse(data['message_id']?.toString() ?? ''); final ts = DateTime.tryParse(data['timestamp']?.toString() ?? ''); if (messageId == null) return; if (!mounted) return; setState(() { for (int i = 0; i < messages.length; i++) { if (messages[i].id == messageId) { messages[i] = messages[i].copyWith(status: MessageStatus.delivered); } } }); if (ts != null) { try { await _localDbService.updateDeliveredAt(messageId, ts); } catch (_) {} } return; } if (data['type'] == 'message_read') { final messageId = int.tryParse(data['message_id'].toString()); if (messageId == null) return; final ts = DateTime.tryParse(data['timestamp']?.toString() ?? ''); if (!mounted) return; setState(() { for (int i = 0; i < messages.length; i++) { if (messages[i].id == messageId) { messages[i] = messages[i].copyWith(status: MessageStatus.read); } } }); if (ts != null) { try { await _localDbService.updateReadAt(messageId, ts); } catch (_) {} } return; } if (data['type'] == 'private_message') { final senderId = int.tryParse(data['sender_id']?.toString() ?? ''); final receiverId = int.tryParse((data['receiver_id'] ?? data['recipient_id'])?.toString() ?? ''); if (senderId == null || receiverId == null) { print('Invalid private_message ids: sender_id=${data['sender_id']} receiver_id=${data['receiver_id'] ?? data['recipient_id']}'); return; } // 1. Проверяем, что сообщение именно от того, с кем мы сейчас общаемся final isFromPartnerToMe = senderId == widget.contact.id && receiverId == myId; if (isFromPartnerToMe) { try { final myPrivKey = await _cryptoService.getPrivateKey(); // 2. Вычисляем общий секрет для расшифровки final sharedSecret = await _cryptoService.deriveSharedSecret( myPrivKey!, widget.contact.publicKey!, ); // 3. Расшифровываем контент final decryptedText = await _cryptoService.decryptMessage( data['content'], sharedSecret, ); // 4. Добавляем в список и обновляем экран await LocalDbService().saveMessages([data]); if (!mounted) return; final serverMessageId = int.tryParse(data['id']?.toString() ?? ''); if (serverMessageId != null && !_sentReadReceipts.contains(serverMessageId)) { Provider.of(context, listen: false).sendReadReceipt(serverMessageId); _sentReadReceipts.add(serverMessageId); } setState(() { messages.add( MessageModel( id: int.tryParse(data['id']?.toString() ?? ''), text: decryptedText, isMe: false, senderId: senderId, receiverId: myId, createdAt: DateTime.parse(data['timestamp']), status: MessageStatus.delivered, replyToId: data['reply_to_id'] == null ? null : int.tryParse(data['reply_to_id'].toString()), replyToText: data['reply_to_text'] == null ? null : data['reply_to_text'].toString(), ), ); }); } catch (e) { print("Ошибка расшифровки входящего сообщения: $e"); } } else { print( "Сообщение от другого пользователя (ID: $senderId), игнорируем в этом чате", ); // Тут можно добавить логику уведомления для списка чатов } } } Future _loadHistory() async { initialMessage = null; // Сбрасываем данные уведомления при загрузке ключа final prefs = await SharedPreferences.getInstance(); await prefs.remove(_notificationLaunchKey); try { final myPrivKey = await _cryptoService.getPrivateKey(); final sharedSecret = await _cryptoService.deriveSharedSecret( myPrivKey!, widget.contact.publicKey!, ); final cached = await _localDbService.getChatHistory(widget.contact.id, myId); try { List loadedLocalMessages = []; for (var msg in cached) { final decrypted = await _cryptoService.decryptMessage( msg['content'], sharedSecret, ); final deliveredAt = msg['delivered_at'] == null ? null : DateTime.tryParse(msg['delivered_at'].toString()); final readAt = msg['read_at'] == null ? null : DateTime.tryParse(msg['read_at'].toString()); MessageStatus status = (msg['sender_id'] == myId) ? MessageStatus.sent : MessageStatus.delivered; if (msg['sender_id'] == myId) { if (readAt != null) { status = MessageStatus.read; } else if (deliveredAt != null) { status = MessageStatus.delivered; } } loadedLocalMessages.add( MessageModel( id: int.tryParse(msg['id']?.toString() ?? ''), text: decrypted, isMe: msg['sender_id'] == myId, senderId: msg['sender_id'], receiverId: msg['receiver_id'], createdAt: DateTime.parse(msg['timestamp']), status: status, replyToId: msg['reply_to_id'] == null ? null : int.tryParse(msg['reply_to_id'].toString()), replyToText: msg['reply_to_text'] == null ? null : msg['reply_to_text'].toString(), ), ); } if (cached.isNotEmpty) { if (!mounted) return; setState(() { messages = loadedLocalMessages; _isKeyLoading = false; }); } } catch (e) { print(e); } final history = await apiService.getChatHistory(widget.contact.id); print(history); final alreadyReadIncomingMessageIds = {}; List loadedMessages = []; for (var msg in history) { final msgId = int.tryParse(msg['id']?.toString() ?? ''); if (msgId != null && msg['sender_id'] != myId && msg['read_at'] != null) { alreadyReadIncomingMessageIds.add(msgId); } final decrypted = await _cryptoService.decryptMessage( msg['content'], sharedSecret, ); final deliveredAt = msg['delivered_at'] == null ? null : DateTime.tryParse(msg['delivered_at'].toString()); final readAt = msg['read_at'] == null ? null : DateTime.tryParse(msg['read_at'].toString()); MessageStatus status = (msg['sender_id'] == myId) ? MessageStatus.sent : MessageStatus.delivered; if (msg['sender_id'] == myId) { if (readAt != null) { status = MessageStatus.read; } else if (deliveredAt != null) { status = MessageStatus.delivered; } } loadedMessages.insert( 0, MessageModel( id: int.tryParse(msg['id']?.toString() ?? ''), text: decrypted, isMe: msg['sender_id'] == myId, senderId: msg['sender_id'], receiverId: msg['receiver_id'], createdAt: DateTime.parse(msg['timestamp']), status: status, replyToId: msg['reply_to_id'] == null ? null : int.tryParse(msg['reply_to_id'].toString()), replyToText: msg['reply_to_text'] == null ? null : msg['reply_to_text'].toString(), ), ); } try { await _localDbService.saveMessages(history); } catch (e) { print("Ошибка сохранения истории в локальную базу: $e"); } if (!mounted) return; setState(() { messages = loadedMessages; _isKeyLoading = false; }); // Отправляем read_receipt для сообщений собеседника, которые уже на экране. for (final m in loadedMessages) { if (m.isMe) continue; final id = m.id; if (id == null) continue; if (alreadyReadIncomingMessageIds.contains(id)) continue; if (_sentReadReceipts.contains(id)) continue; Provider.of(context, listen: false).sendReadReceipt(id); _sentReadReceipts.add(id); } } catch (e) { print("Ошибка загрузки истории: $e"); if (!mounted) return; setState(() => _isKeyLoading = false); } } }