Chepuhagram/lib/presentation/screens/chat_screen.dart

347 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
class ChatScreen extends StatefulWidget {
final Contact contact;
const ChatScreen({super.key, required this.contact});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
static const String _notificationLaunchKey = 'notification_launch_data';
int myId = 0;
late Contact _currentContact;
bool _isKeyLoading = false;
final TextEditingController _controller = TextEditingController();
final ContactRepository _contactRepository = ContactRepository();
final apiService = ApiService();
final CryptoService _cryptoService = CryptoService();
List<MessageModel> messages = [];
StreamSubscription<dynamic>? _socketSubscription;
@override
void initState() {
super.initState();
_currentContact = widget.contact;
currentActiveChatContactId = _currentContact.id; // Устанавливаем активный чат
final contactProvider = context.read<ContactProvider>();
myId = contactProvider.getCurrentUserId() ?? 0;
// Если ключа нет, загружаем его при входе
if (_currentContact.publicKey == null) {
_loadContactKey();
}
_loadHistory();
final socketService = Provider.of<SocketService>(context, listen: false);
_socketSubscription = socketService.messages.listen(_handleIncomingMessage);
}
Future<void> _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();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
} else {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const ContactsScreen()),
);
}
},
),
title: 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.text,
time: msg.createdAt,
isMe: msg.isMe,
);
},
),
),
_buildMessageInput(),
],
),
);
}
Widget _buildMessageInput() {
return SafeArea(
// Добавляем SafeArea здесь
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(
hintText: "Напиши сообщение...",
),
),
),
IconButton(
icon: const Icon(Icons.send),
onPressed: () {
_sendMessage();
},
),
],
),
),
);
}
Future<void> _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,
);
// Формируем payload для сервера
final payload = {
"type": "private_message",
"receiver_id": _currentContact.id,
"content": encryptedText,
"content50": encryptedText50,
};
// Отправляем
print("ОТПРАВКА: $payload");
Provider.of<SocketService>(context, listen: false).sendMessage(payload);
// Обновляем UI (себе показываем расшифрованный текст)
setState(() {
messages.add(
MessageModel(
text: rawText,
isMe: true,
senderId: myId,
receiverId: _currentContact.id,
createdAt: DateTime.now(),
),
);
});
_controller.clear();
} catch (e) {
_controller.text = rawText;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text("Ошибка шифрования: $e")));
}
}
void _handleIncomingMessage(Map<String, dynamic> data) async {
if (data['type'] == 'private_message') {
final int senderId = int.parse(data['sender_id'].toString());
// 1. Проверяем, что сообщение именно от того, с кем мы сейчас общаемся
if (senderId == widget.contact.id) {
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;
setState(() {
messages.add(
MessageModel(
text: decryptedText,
isMe: false,
senderId: senderId,
receiverId: myId,
createdAt: DateTime.parse(data['timestamp']),
),
);
});
} catch (e) {
print("Ошибка расшифровки входящего сообщения: $e");
}
} else {
print(
"Сообщение от другого пользователя (ID: $senderId), игнорируем в этом чате",
);
// Тут можно добавить логику уведомления для списка чатов
}
}
}
Future<void> _loadHistory() async {
initialMessage = null; // Сбрасываем данные уведомления при загрузке ключа
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_notificationLaunchKey);
await prefs.setString(_notificationLaunchKey, ''); // Очищаем данные уведомления при загрузке ключа
try {
final myPrivKey = await _cryptoService.getPrivateKey();
final sharedSecret = await _cryptoService.deriveSharedSecret(
myPrivKey!,
widget.contact.publicKey!,
);
final localDb = LocalDbService();
final cached = await localDb.getChatHistory(widget.contact.id, myId);
try {
List<MessageModel> loadedLocalMessages = [];
for (var msg in cached) {
final decrypted = await _cryptoService.decryptMessage(
msg['content'],
sharedSecret,
);
loadedLocalMessages.add(
MessageModel(
text: decrypted,
isMe: msg['sender_id'] == myId,
senderId: msg['sender_id'],
receiverId: msg['receiver_id'],
createdAt: DateTime.parse(msg['timestamp']),
),
);
}
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);
List<MessageModel> loadedMessages = [];
for (var msg in history) {
final decrypted = await _cryptoService.decryptMessage(
msg['content'],
sharedSecret,
);
loadedMessages.insert(
0,
MessageModel(
text: decrypted,
isMe: msg['sender_id'] == myId,
senderId: msg['sender_id'],
receiverId: msg['receiver_id'],
createdAt: DateTime.parse(msg['timestamp']),
),
);
}
try {
await localDb.saveMessages(history);
} catch (e) {
print("Ошибка сохранения истории в локальную базу: $e");
}
if (!mounted) return;
setState(() {
messages = loadedMessages;
_isKeyLoading = false;
});
} catch (e) {
print("Ошибка загрузки истории: $e");
if (!mounted) return;
setState(() => _isKeyLoading = false);
}
}
}