307 lines
9.1 KiB
Dart
307 lines
9.1 KiB
Dart
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 'dart:convert';
|
||
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';
|
||
|
||
class ChatScreen extends StatefulWidget {
|
||
final Contact contact;
|
||
|
||
const ChatScreen({super.key, required this.contact});
|
||
|
||
@override
|
||
State<ChatScreen> createState() => _ChatScreenState();
|
||
}
|
||
|
||
class _ChatScreenState extends State<ChatScreen> {
|
||
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 = [];
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_currentContact = widget.contact;
|
||
final contactProvider = context.read<ContactProvider>();
|
||
myId = contactProvider.getCurrentUserId() ?? 0;
|
||
_loadHistory();
|
||
// Если ключа нет, загружаем его при входе
|
||
if (_currentContact.publicKey == null) {
|
||
_loadContactKey();
|
||
}
|
||
}
|
||
|
||
Future<void> _loadContactKey() async {
|
||
setState(() => _isKeyLoading = true);
|
||
try {
|
||
final updatedContact = await _contactRepository.fetchContactById(
|
||
_currentContact.id,
|
||
);
|
||
setState(() {
|
||
_currentContact = updatedContact;
|
||
_isKeyLoading = false;
|
||
});
|
||
print(updatedContact.publicKey);
|
||
} catch (e) {
|
||
setState(() => _isKeyLoading = false);
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(
|
||
content: Text("Не удалось получить ключ шифрования собеседника"),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_controller.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(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,
|
||
);
|
||
|
||
// Формируем payload для сервера
|
||
final payload = {
|
||
"type": "private_message",
|
||
"receiver_id": _currentContact.id,
|
||
"content": encryptedText,
|
||
};
|
||
|
||
// Отправляем
|
||
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")));
|
||
}
|
||
}
|
||
|
||
@override
|
||
void didChangeDependencies() {
|
||
super.didChangeDependencies();
|
||
// Подписываемся на поток сообщений из сокета
|
||
final socketService = Provider.of<SocketService>(context, listen: false);
|
||
|
||
socketService.messages.listen((rawData) {
|
||
_handleIncomingMessage(rawData);
|
||
});
|
||
}
|
||
|
||
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]);
|
||
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 {
|
||
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) {
|
||
setState(() {
|
||
messages = loadedLocalMessages;
|
||
_isKeyLoading = false;
|
||
});
|
||
}
|
||
} catch (e) {
|
||
print(e);
|
||
}
|
||
|
||
final history = await apiService.getChatHistory(widget.contact.id);
|
||
|
||
List<MessageModel> loadedMessages = [];
|
||
for (var msg in history) {
|
||
final decrypted = await _cryptoService.decryptMessage(
|
||
msg['content'],
|
||
sharedSecret,
|
||
);
|
||
loadedMessages.add(
|
||
MessageModel(
|
||
text: decrypted,
|
||
isMe: msg['sender_id'] == myId,
|
||
senderId: msg['sender_id'],
|
||
receiverId: msg['receiver_id'],
|
||
createdAt: DateTime.parse(msg['timestamp']),
|
||
),
|
||
);
|
||
}
|
||
await localDb.saveMessages(history);
|
||
|
||
setState(() {
|
||
messages = loadedMessages;
|
||
_isKeyLoading = false;
|
||
});
|
||
} catch (e) {
|
||
print("Ошибка загрузки истории: $e");
|
||
setState(() => _isKeyLoading = false);
|
||
}
|
||
}
|
||
}
|