diff --git a/lib/data/models/contact_model.dart b/lib/data/models/contact_model.dart index 5384dfa..7155161 100644 --- a/lib/data/models/contact_model.dart +++ b/lib/data/models/contact_model.dart @@ -9,6 +9,7 @@ class Contact { final bool isOnline; final int unreadCount; final String? publicKey; + final bool isLastMsgDecrypted; Contact({ required this.id, @@ -21,6 +22,7 @@ class Contact { this.isOnline = false, this.unreadCount = 0, this.publicKey, + this.isLastMsgDecrypted = false, }); Contact copyWith({ @@ -34,6 +36,7 @@ class Contact { bool? isOnline, int? unreadCount, String? publicKey, + bool? isLastMsgDecrypted, }) { return Contact( id: id ?? this.id, @@ -46,6 +49,7 @@ class Contact { isOnline: isOnline ?? this.isOnline, unreadCount: unreadCount ?? this.unreadCount, publicKey: publicKey ?? this.publicKey, + isLastMsgDecrypted: isLastMsgDecrypted ?? this.isLastMsgDecrypted, ); } @@ -68,6 +72,7 @@ class Contact { isOnline: (json['is_online'] ?? json['isOnline']) == true, unreadCount: int.tryParse((json['unread_count'] ?? json['unreadCount'] ?? 0).toString()) ?? 0, publicKey: json['public_key'], + isLastMsgDecrypted: json['is_last_msg_decrypted'] ?? false, ); } } diff --git a/lib/data/repositories/contact_repository.dart b/lib/data/repositories/contact_repository.dart index 9061658..a0acc86 100644 --- a/lib/data/repositories/contact_repository.dart +++ b/lib/data/repositories/contact_repository.dart @@ -10,6 +10,12 @@ class ContactRepository { Future> fetchChatContacts() async { final token = await _apiService.getAccessToken(); + + + DateTime now = DateTime.now(); + + Duration offset = now.timeZoneOffset; + if (token == null) { throw Exception('No access token'); } @@ -24,7 +30,14 @@ class ContactRepository { if (response.statusCode == 200) { final List data = jsonDecode(utf8.decode(response.bodyBytes)); - return data.map((json) => Contact.fromJson(json)).toList(); + List contacts = data.map((json) => Contact.fromJson(json)).toList(); + for (var item in contacts) { + if (item.lastMessageTime != null) { + DateTime serverTime = item.lastMessageTime!; + item = item.copyWith(lastMessageTime: serverTime.add(offset)); + } + } + return contacts; } else { throw Exception('Failed to load contacts'); } @@ -32,6 +45,10 @@ class ContactRepository { Future> fetchAllUsers() async { final token = await _apiService.getAccessToken(); + + DateTime now = DateTime.now(); + Duration offset = now.timeZoneOffset; + if (token == null) { throw Exception('No access token'); } @@ -46,7 +63,14 @@ class ContactRepository { if (response.statusCode == 200) { final List data = jsonDecode(utf8.decode(response.bodyBytes)); - return data.map((json) => Contact.fromJson(json)).toList(); + List contacts = data.map((json) => Contact.fromJson(json)).toList(); + for (var contact in contacts) { + if (contact.lastMessageTime != null) { + DateTime serverTime = contact.lastMessageTime!; + contact = contact.copyWith(lastMessageTime: serverTime.add(offset)); + } + } + return contacts; } else { throw Exception('Failed to load contacts'); } diff --git a/lib/domain/services/crypto_service.dart b/lib/domain/services/crypto_service.dart index 841032a..3a627d7 100644 --- a/lib/domain/services/crypto_service.dart +++ b/lib/domain/services/crypto_service.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'package:cryptography/cryptography.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'dart:convert'; +import 'package:chepuhagram/data/models/contact_model.dart'; class CryptoService { final _storage = const FlutterSecureStorage(); @@ -123,6 +124,117 @@ class CryptoService { return base64Encode(nonce + encrypted.mac.bytes + encrypted.cipherText); } + static Future decryptInIsolate( + String base64Data, + SecretKey sharedKey, + ) async { + final data = base64Decode(base64Data); + final aesGcm = AesGcm.with256bits(); + + final nonce = data.sublist(0, 12); + final mac = data.sublist(12, 28); + final cipherText = data.sublist(28); + + final decrypted = await aesGcm.decrypt( + SecretBox(cipherText, nonce: nonce, mac: Mac(mac)), + secretKey: sharedKey, + ); + + return utf8.decode(decrypted); + } + + static Future> bulkDecryptContacts( + Map data, + ) async { + final List contacts = data['contacts']; + final String privKey = data['privKey']; + final Map cache = data['cache']; + + final x25519 = X25519(); + final aesGcm = AesGcm.with256bits(); + final List result = []; + + // Вычисляем свою пару один раз + final myKeyPair = await x25519.newKeyPairFromSeed(base64Decode(privKey)); + + for (var contact in contacts) { + if (contact.lastMessage == null || contact.publicKey == null) { + result.add(contact); + continue; + } + + try { + SecretKey sharedKey; + if (cache.containsKey(contact.id)) { + sharedKey = cache[contact.id]!; + } else { + final theirPubKey = SimplePublicKey( + base64Decode(contact.publicKey!), + type: KeyPairType.x25519, + ); + sharedKey = await x25519.sharedSecretKey( + keyPair: myKeyPair, + remotePublicKey: theirPubKey, + ); + } + + // Дешифровка AES-GCM + final msgData = base64Decode(contact.lastMessage!); + final decrypted = await aesGcm.decrypt( + SecretBox( + msgData.sublist(28), + nonce: msgData.sublist(0, 12), + mac: Mac(msgData.sublist(12, 28)), + ), + secretKey: sharedKey, + ); + + result.add( + contact.copyWith( + lastMessage: utf8.decode(decrypted), + isLastMsgDecrypted: true, + ), + ); + } catch (_) { + result.add(contact); + } + } + return result; + } + + static Future>> computeSharedKeysTask( + Map params, + ) async { + final Map isolateKeysMap = params['keysMap']; + final String isolatePrivKey = params['privKey']; + + final x25519 = X25519(); + final Map> result = {}; + + final myKeyPair = await x25519.newKeyPairFromSeed( + base64Decode(isolatePrivKey), + ); + + for (var entry in isolateKeysMap.entries) { + try { + final theirPubKey = SimplePublicKey( + base64Decode(entry.value), + type: KeyPairType.x25519, + ); + + final sharedKey = await x25519.sharedSecretKey( + keyPair: myKeyPair, + remotePublicKey: theirPubKey, + ); + + result[entry.key] = await sharedKey.extractBytes(); + } catch (_) { + continue; + } + } + return result; + } + Future<(List, String)?> encryptImage( List fileBytes, SecretKey sharedKey, diff --git a/lib/logic/contact_provider.dart b/lib/logic/contact_provider.dart index 69e1479..09501ef 100644 --- a/lib/logic/contact_provider.dart +++ b/lib/logic/contact_provider.dart @@ -3,11 +3,18 @@ import '/data/models/contact_model.dart'; import '/data/repositories/contact_repository.dart'; import '/data/datasources/local_db_service.dart'; import '/domain/services/crypto_service.dart'; +import 'dart:isolate'; +import 'dart:convert'; +import 'package:cryptography/cryptography.dart'; +import 'package:flutter/foundation.dart'; class ContactProvider extends ChangeNotifier { final ContactRepository _repository = ContactRepository(); final LocalDbService _localDbService = LocalDbService(); - final CryptoService _cryptoService = CryptoService(); + final CryptoService _cryptoService; + + ContactProvider(this._cryptoService); + final Map _sharedKeysCache = {}; List _contacts = []; List _allContacts = []; bool _isLoading = false; @@ -19,6 +26,11 @@ class ContactProvider extends ChangeNotifier { List get allContacts => _allContacts; bool get isLoading => _isLoading; String? get error => _error; + Map get sharedKeysCache => _sharedKeysCache; + + void setSharedKey(int contactId, SecretKey key) { + _sharedKeysCache[contactId] = key; + } void setCurrentUserId(int? id) { _currentUserId = id; @@ -29,7 +41,7 @@ class ContactProvider extends ChangeNotifier { return _currentUserId; } - Future loadContacts() async { + Future loadContacts({bool enrichContacts = true}) async { if (_isFirstLoad) { _isFirstLoad = false; _isLoading = true; @@ -39,21 +51,23 @@ class ContactProvider extends ChangeNotifier { try { final allContacts = await _repository.fetchChatContacts(); - // Фильтруем: исключаем себя (для основного списка - только чаты) - _contacts = allContacts - .where((contact) => contact.id != _currentUserId) - .toList(); + final userIdCopy = _currentUserId; + _contacts = await Isolate.run(() { + return allContacts + .where((contact) => contact.id != userIdCopy) + .toList(); + }); _allContacts = _contacts; _isLoading = false; notifyListeners(); - // Обогащаем превью последним сообщением из локальной БД, не блокируя UI. - _enrichContactsWithLastMessages(); + if (enrichContacts) { + await _enrichContactsWithLastMessages(); + } } catch (e) { _error = e.toString(); + print("❌ ОШИБКА ПРИ ЗАГРУЗКЕ КОНТАКТОВ: $_error"); } finally { - // Если ошибка — выходим из состояния загрузки тут. - // Если всё ок — `_isLoading` уже сброшен выше, чтобы показать список быстрее. if (_error != null) { _isLoading = false; } @@ -61,7 +75,6 @@ class ContactProvider extends ChangeNotifier { } } - // Метод для получения всех контактов (исключая себя) для нового чата Future loadAllContactsForNewChat() async { _isLoading = true; _error = null; @@ -69,7 +82,6 @@ class ContactProvider extends ChangeNotifier { try { final allContacts = await _repository.fetchAllUsers(); - // Фильтруем только исключение самого себя _allContacts = allContacts .where((contact) => contact.id != _currentUserId) .toList(); @@ -82,42 +94,30 @@ class ContactProvider extends ChangeNotifier { } Future _enrichContactsWithLastMessages() async { - print("Начинаем обогащать контакты последними сообщениями из локальной БД... Для текущего пользователя ID: $_currentUserId"); - final myId = _currentUserId; - if (myId == null) return; - print("Текущий пользователь ID: $myId"); + final myPrivKeyBase64 = await _cryptoService.getPrivateKey(); + if (myPrivKeyBase64 == null) return; - final myPrivKey = await _cryptoService.getPrivateKey(); + // Создаем локальные копии для передачи + final contactsToProcess = List.from(_contacts); + final cacheCopy = Map.from(_sharedKeysCache); - final List updated = List.from(_contacts); + print('Avialable cache for contacts: ${cacheCopy.length}'); - for (int i = 0; i < updated.length; i++) { - final contact = updated[i]; + try { + final updatedContacts = await compute( + CryptoService.bulkDecryptContacts, + { + 'contacts': contactsToProcess, + 'privKey': myPrivKeyBase64, + 'cache': cacheCopy, + }, + ); - // 1) Если сервер уже прислал lastMessage — попробуем расшифровать превью. - print(contact.lastMessage); - if (contact.lastMessage != null && - contact.lastMessage!.isNotEmpty && - myPrivKey != null && - contact.publicKey != null) { - try { - final sharedSecret = await _cryptoService.deriveSharedSecret( - myPrivKey, - contact.publicKey!, - ); - final decrypted = await _cryptoService.decryptMessage( - contact.lastMessage!, - sharedSecret, - ); - updated[i] = contact.copyWith(lastMessage: decrypted); - } catch (_) { - // Если расшифровать не удалось — оставляем как есть, дальше попробуем локальную БД. - } - } - } - - _contacts = updated; - _allContacts = updated; + _contacts = updatedContacts; notifyListeners(); + } catch (e) { + print("Ошибка дешифровки: $e"); } } + +} diff --git a/lib/main.dart b/lib/main.dart index 50a5340..a2466b5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,15 +22,12 @@ final GlobalKey navigatorKey = GlobalKey(); final RouteObserver routeObserver = RouteObserver(); -// Глобальная переменная для отслеживания текущего активного контакта в чате int? currentActiveChatContactId; -// Глобальная переменная для хранения начального сообщения (при запуске из уведомления) RemoteMessage? initialMessage; // Ключ для SharedPreferences const String _notificationLaunchKey = 'notification_launch_data'; -// Защита от повторной обработки одного и того же payload при следующих запусках по иконке const String _lastHandledNotificationLaunchPayloadKey = 'notification_last_handled_payload'; @@ -49,9 +46,6 @@ Future _onSelectNotification( final prefs = await SharedPreferences.getInstance(); final canonicalPayload = jsonEncode(data); - // Важно: не сохраняем payload в SharedPreferences, если можем сразу перейти в чат. - // Иначе при следующем обычном запуске (по иконке) останется "хвост" и приложение - // будет снова автопереходить в чат. if (context == null) { final lastHandled = prefs.getString( _lastHandledNotificationLaunchPayloadKey, @@ -70,7 +64,6 @@ Future _onSelectNotification( await prefs.remove(_notificationLaunchKey); } - // Navigate to chat with this contact (if context is ready) _navigateToChat(senderId); } else { print( @@ -135,8 +128,6 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); - // Проверяем, было ли приложение запущено из уведомления - // Добавляем небольшую задержку, чтобы Firebase полностью инициализировался await Future.delayed(const Duration(milliseconds: 500)); initialMessage = await FirebaseMessaging.instance.getInitialMessage(); print('Initial message from main() after delay: $initialMessage'); @@ -226,10 +217,17 @@ void main() async { runApp( MultiProvider( providers: [ + Provider(create: (_) => CryptoService()), + Provider(create: (_) => SocketService()), ChangeNotifierProvider(create: (_) => AuthProvider()), ChangeNotifierProvider(create: (_) => ThemeProvider()), - ChangeNotifierProvider(create: (_) => ContactProvider()), Provider(create: (_) => SocketService()), + + ChangeNotifierProvider( + create: (context) => ContactProvider( + context.read(), + ), + ), ], child: const MyApp(), ), diff --git a/lib/presentation/screens/chat_screen.dart b/lib/presentation/screens/chat_screen.dart index 2205294..09a9064 100644 --- a/lib/presentation/screens/chat_screen.dart +++ b/lib/presentation/screens/chat_screen.dart @@ -494,6 +494,10 @@ class _ChatScreenState extends State { } _replyTo = null; }); + Navigator.push( + context, + MaterialPageRoute(builder: (context) => ChatScreen(contact: targetContact,)), + ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( @@ -754,11 +758,16 @@ class _ChatScreenState extends State { } void _handleIncomingMessage(Map data) async { + DateTime now = DateTime.now(); + + Duration offset = now.timeZoneOffset; // 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() ?? ''); + var ts = DateTime.tryParse( + data['timestamp']?.toString() ?? '', + )?.add(offset); if (tempId == null) return; @@ -779,7 +788,9 @@ class _ChatScreenState extends State { 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() ?? ''); + var ts = DateTime.tryParse( + data['timestamp']?.toString() ?? '', + )?.add(offset); if (tempId == null) return; @@ -799,7 +810,9 @@ class _ChatScreenState extends State { // Доставка онлайн (получатель был в сети) if (data['type'] == 'message_delivered') { final messageId = int.tryParse(data['message_id']?.toString() ?? ''); - final ts = DateTime.tryParse(data['timestamp']?.toString() ?? ''); + var ts = DateTime.tryParse( + data['timestamp']?.toString() ?? '', + )?.add(offset); if (messageId == null) return; if (!mounted) return; @@ -821,7 +834,9 @@ class _ChatScreenState extends State { if (data['type'] == 'message_edited') { final messageId = int.tryParse(data['message_id']?.toString() ?? ''); - final ts = DateTime.tryParse(data['edited_at']?.toString() ?? ''); + var ts = DateTime.tryParse( + data['edited_at']?.toString() ?? '', + )?.add(offset); if (messageId == null) return; final myPrivKey = await _cryptoService.getPrivateKey(); @@ -871,7 +886,9 @@ class _ChatScreenState extends State { if (data['type'] == 'message_read') { final messageId = int.tryParse(data['message_id'].toString()); if (messageId == null) return; - final ts = DateTime.tryParse(data['timestamp']?.toString() ?? ''); + var ts = DateTime.tryParse( + data['timestamp']?.toString() ?? '', + )?.add(offset); if (!mounted) return; setState(() { @@ -943,15 +960,14 @@ class _ChatScreenState extends State { isMe: false, senderId: senderId, receiverId: myId, - createdAt: DateTime.parse(data['timestamp']), + createdAt: DateTime.parse(data['timestamp']).add(offset), status: MessageStatus.delivered, replyToId: data['reply_to_id'] == null ? null : int.tryParse(data['reply_to_id'].toString()), - replyToText: - data['reply_to_text'] != null - ? data['reply_to_text'].toString() - : null, + replyToText: data['reply_to_text'] != null + ? data['reply_to_text'].toString() + : null, ), ); }); @@ -968,6 +984,9 @@ class _ChatScreenState extends State { } Future _loadHistory() async { + DateTime now = DateTime.now(); + + Duration offset = now.timeZoneOffset; initialMessage = null; // Сбрасываем данные уведомления при загрузке ключа final prefs = await SharedPreferences.getInstance(); await prefs.remove(_notificationLaunchKey); @@ -992,10 +1011,10 @@ class _ChatScreenState extends State { final deliveredAt = msg['delivered_at'] == null ? null - : DateTime.tryParse(msg['delivered_at'].toString()); + : DateTime.tryParse(msg['delivered_at'].toString())?.add(offset); final readAt = msg['read_at'] == null ? null - : DateTime.tryParse(msg['read_at'].toString()); + : DateTime.tryParse(msg['read_at'].toString())?.add(offset); MessageStatus status = (msg['sender_id'] == myId) ? MessageStatus.sent @@ -1015,7 +1034,7 @@ class _ChatScreenState extends State { isMe: msg['sender_id'] == myId, senderId: msg['sender_id'], receiverId: msg['receiver_id'], - createdAt: DateTime.parse(msg['timestamp']), + createdAt: DateTime.parse(msg['timestamp']).add(offset), status: status, replyToId: msg['reply_to_id'] == null ? null @@ -1024,7 +1043,7 @@ class _ChatScreenState extends State { ? msg['reply_to_text'].toString() : null, editedAt: msg['edited_at'] != null - ? DateTime.tryParse(msg['edited_at'].toString()) + ? DateTime.tryParse(msg['edited_at'].toString())?.add(offset) : null, ), ); @@ -1059,10 +1078,10 @@ class _ChatScreenState extends State { final deliveredAt = msg['delivered_at'] == null ? null - : DateTime.tryParse(msg['delivered_at'].toString()); + : DateTime.tryParse(msg['delivered_at'].toString())?.add(offset); final readAt = msg['read_at'] == null ? null - : DateTime.tryParse(msg['read_at'].toString()); + : DateTime.tryParse(msg['read_at'].toString())?.add(offset); MessageStatus status = (msg['sender_id'] == myId) ? MessageStatus.sent @@ -1083,7 +1102,7 @@ class _ChatScreenState extends State { isMe: msg['sender_id'] == myId, senderId: msg['sender_id'], receiverId: msg['receiver_id'], - createdAt: DateTime.parse(msg['timestamp']), + createdAt: DateTime.parse(msg['timestamp']).add(offset), status: status, replyToId: msg['reply_to_id'] == null ? null @@ -1092,12 +1111,13 @@ class _ChatScreenState extends State { ? msg['reply_to_text'].toString() : null, editedAt: msg['edited_at'] != null - ? DateTime.tryParse(msg['edited_at'].toString()) + ? DateTime.tryParse(msg['edited_at'].toString())?.add(offset) : null, ), ); } try { + await _localDbService.deleteChatHistory(widget.contact.id, myId); await _localDbService.saveMessages(history); } catch (e) { print("Ошибка сохранения истории в локальную базу: $e"); @@ -1119,7 +1139,6 @@ class _ChatScreenState extends State { Provider.of(context, listen: false).sendReadReceipt(id); _sentReadReceipts.add(id); } - await _localDbService.deleteChatHistory(widget.contact.id, myId); } catch (e) { print("Ошибка загрузки истории: $e"); if (!mounted) return; diff --git a/lib/presentation/screens/contacts_screen.dart b/lib/presentation/screens/contacts_screen.dart index cba199a..6797b68 100644 --- a/lib/presentation/screens/contacts_screen.dart +++ b/lib/presentation/screens/contacts_screen.dart @@ -55,21 +55,28 @@ class _ContactsScreenState extends State with RouteAware { 'Setting current user ID in ContactProvider: ${authProvider.currentUserId}', ); contactProvider.setCurrentUserId(authProvider.currentUserId); - contactProvider.loadContacts().then((_) { - print('Contacts loaded, checking targetChatId: ${widget.targetChatId}'); - // После загрузки контактов проверить, нужно ли перейти к чату - if (widget.targetChatId != null) { - _navigateToTargetChat(); - } else { - _checkSavedNotificationTarget(); - } - }); + _initContacts(); }); WidgetsBinding.instance.addPostFrameCallback((_) { _checkAppUpdate(); }); } + Future _initContacts() async { + final contactProvider = context.read(); + // Ждем завершения загрузки контактов + await contactProvider.loadContacts(); + + print('Contacts loaded, checking targetChatId: ${widget.targetChatId}'); + + // Дальнейшая логика выполнится только после того, как loadContacts завершится + if (widget.targetChatId != null) { + _navigateToTargetChat(); + } else { + _checkSavedNotificationTarget(); + } + } + @override void didChangeDependencies() { super.didChangeDependencies(); diff --git a/lib/presentation/screens/splash_screen.dart b/lib/presentation/screens/splash_screen.dart index 1f63a06..8326522 100644 --- a/lib/presentation/screens/splash_screen.dart +++ b/lib/presentation/screens/splash_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -15,6 +16,13 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:chepuhagram/main.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; +import 'package:chepuhagram/domain/services/crypto_service.dart'; +import 'package:cryptography/cryptography.dart'; +import 'package:chepuhagram/data/repositories/contact_repository.dart '; +import 'package:chepuhagram/data/models/contact_model.dart'; +import 'dart:isolate'; +import 'package:flutter/foundation.dart'; +import 'package:convert/convert.dart'; class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); @@ -29,6 +37,8 @@ class _SplashScreenState extends State { // Ключ для SharedPreferences static const String _notificationLaunchKey = 'notification_launch_data'; + static const String _contactPublicKey = 'contact_public_key_'; + static const String _contactSharedKey = 'contact_shared_key_'; @override void initState() { @@ -117,12 +127,59 @@ class _SplashScreenState extends State { // Проверяем, было ли приложение запущено из уведомления int? targetChatId = _targetChatId; // Сначала проверяем из onMessageOpenedApp - - // Если не установлено, проверяем SharedPreferences if (targetChatId == null) { final prefs = await SharedPreferences.getInstance(); final savedData = prefs.getString(_notificationLaunchKey); + try { + final contactProvider = context.read(); + contactProvider.setCurrentUserId(authProvider.currentUserId); + + await contactProvider.loadContacts(enrichContacts: false); + + final myPrivKeyBase64 = await context + .read() + .getPrivateKey(); + if (myPrivKeyBase64 != null) { + final Map keysToCompute = {}; + for (var c in contactProvider.contacts) { + final savedKeyHex = prefs.getString( + '$_contactSharedKey${c.id}', + ); + final savedPubKey = prefs.getString( + '$_contactPublicKey${c.id}', + ); + if (savedKeyHex != null && savedPubKey == c.publicKey) { + final bytes = base64Decode( + savedKeyHex, + ); + contactProvider.setSharedKey(c.id, SecretKey(bytes)); + } else if (c.publicKey != null) { + keysToCompute[c.id] = c.publicKey!; + } + } + print( + 'Contacts with keys for isolate: ${keysToCompute.keys.toList()}', + ); + + final String privKey = myPrivKeyBase64; + final computedKeys = await compute( + CryptoService.computeSharedKeysTask, + {'keysMap': keysToCompute, 'privKey': privKey}, + ); + + computedKeys.forEach((id, bytes) { + contactProvider.setSharedKey(id, SecretKey(bytes)); + prefs.setString('$_contactSharedKey$id', base64Encode(bytes)); + prefs.setString('$_contactPublicKey$id', keysToCompute[id]!); + }); + } + } catch (e) { + print("Ошибка при загрузке контактов или вычислении ключей: $e"); + } + + // Если не установлено, проверяем SharedPreferences + if (savedData != null) { try { final data = jsonDecode(savedData) as Map; @@ -178,7 +235,7 @@ class _SplashScreenState extends State { try { final contactProvider = context.read(); contactProvider.setCurrentUserId(authProvider.currentUserId); - await contactProvider.loadContacts(); + await contactProvider.loadContacts(enrichContacts: false); final contact = contactProvider.contacts.firstWhere( (c) => c.id == targetChatId, diff --git a/lib/presentation/widgets/contact_tile.dart b/lib/presentation/widgets/contact_tile.dart index c4b7f8f..b024024 100644 --- a/lib/presentation/widgets/contact_tile.dart +++ b/lib/presentation/widgets/contact_tile.dart @@ -46,7 +46,7 @@ class ContactTile extends StatelessWidget { style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), subtitle: Text( - contact.lastMessage ?? "Нет сообщений", + contact.isLastMsgDecrypted ? contact.lastMessage ?? "Нет сообщений" : (contact.lastMessage != null ? "Ожидание дешифровки..." : "Нет сообщений"), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: Colors.grey), diff --git a/pubspec.lock b/pubspec.lock index 0d2d4b2..6b345b0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + convert: + dependency: "direct main" + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" cross_file: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9ee5eff..39186d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,7 @@ dependencies: dio: ^5.9.2 package_info_plus: ^9.0.1 open_filex: ^4.3.2 + convert: ^3.1.2 dev_dependencies: flutter_test: diff --git a/srv/app/api/endpoints/users.py b/srv/app/api/endpoints/users.py index 91b3106..d023bc4 100644 --- a/srv/app/api/endpoints/users.py +++ b/srv/app/api/endpoints/users.py @@ -6,7 +6,7 @@ from app.core.security import get_current_user from app.api import schemas from sqlalchemy import or_, and_, exists from sqlalchemy.exc import IntegrityError - +from app.websocket import connection_manager # бд def get_db(): @@ -146,7 +146,8 @@ async def update_privacy_settings( user_to_update.show_about = 1 if data.show_about else 0 if data.show_username is not None: user_to_update.show_username = 1 if data.show_username else 0 - + if data.show_last_online is not None: + user_to_update.show_last_online = 1 if data.show_last_online else 0 try: db.commit() except Exception: @@ -169,6 +170,7 @@ async def get_privacy_settings(current_user: models.User = Depends(get_current_u "show_avatar": bool(current_user.show_avatar), "show_about": bool(current_user.show_about), "show_username": bool(current_user.show_username), + "show_last_online": bool(current_user.show_last_online), } @@ -288,5 +290,12 @@ def get_user_by_id( if user.show_email: profile_data["email"] = user.email + + if user.id in connection_manager.active_connections: + profile_data["online"] = True + else: + profile_data["online"] = False + if user.show_last_online: + profile_data["last_online"] = user.last_online.isoformat() if user.last_online else None return profile_data diff --git a/srv/app/db/models.py b/srv/app/db/models.py index 9c78909..f0b44da 100644 --- a/srv/app/db/models.py +++ b/srv/app/db/models.py @@ -32,6 +32,8 @@ class User(Base): show_avatar = Column(Integer, nullable=False, server_default="1") show_about = Column(Integer, nullable=False, server_default="1") show_username = Column(Integer, nullable=False, server_default="1") + show_last_online = Column(Integer, nullable=False, server_default="1") + last_online = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) class Message(Base): __tablename__ = "messages" @@ -93,6 +95,11 @@ def _ensure_sqlite_user_columns(): conn.execute(text("ALTER TABLE users ADD COLUMN show_about INTEGER DEFAULT 1")) if "show_username" not in existing: conn.execute(text("ALTER TABLE users ADD COLUMN show_username INTEGER DEFAULT 1")) + if "show_last_online" not in existing: + conn.execute(text("ALTER TABLE users ADD COLUMN show_last_online INTEGER DEFAULT 1")) + if "last_online" not in existing: + conn.execute(text("ALTER TABLE users ADD COLUMN last_online DATETIME")) + conn.execute(text("UPDATE users SET last_online = datetime('now')")) conn.commit() diff --git a/srv/app/websocket/connection_manager.py b/srv/app/websocket/connection_manager.py index b494769..7dd6816 100644 --- a/srv/app/websocket/connection_manager.py +++ b/srv/app/websocket/connection_manager.py @@ -1,14 +1,15 @@ from fastapi import HTTPException, status, APIRouter, WebSocket, WebSocketDisconnect, Query, Depends from app.core.security import test_token from typing import Dict -from datetime import datetime +from datetime import datetime, timezone import json from sqlalchemy.orm import Session from app.db import models from firebase_admin import messaging, credentials, exceptions import firebase_admin -cred = credentials.Certificate("chepuhagram-6ca5d-firebase-adminsdk-fbsvc-cf8a5ad2f3.json") +cred = credentials.Certificate( + "chepuhagram-6ca5d-firebase-adminsdk-fbsvc-cf8a5ad2f3.json") firebase_admin.initialize_app(cred) # бд @@ -40,16 +41,25 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db: print("ПОДКЛЮЧЕНИЕ") await manager.connect(user_id, websocket) print("ПОДКЛЮЧЕНО") + + db.query(models.User).filter(models.User.id == user_id).update({"last_online": datetime.now(timezone.utc)}, + synchronize_session="fetch") + db.commit() try: while True: print("ОЖИДАНИЕ СООБЩЕНИЙ") data = await websocket.receive_text() message_data = json.loads(data) print(f"DEBUG: Получены данные: {message_data}") + + db.query(models.User).filter(models.User.id == user_id).update({"last_online": datetime.now(timezone.utc)}, + synchronize_session="fetch") + db.commit() if message_data.get("type") == "private_message": - - user = db.query(models.User).filter(models.User.id == user_id).first() + + user = db.query(models.User).filter( + models.User.id == user_id).first() receiver_id = message_data.get("receiver_id") temp_id = message_data.get("temp_id") content = message_data.get("content") @@ -150,7 +160,8 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db: "detail": "message_id must be int", }) continue - msg = db.query(models.Message).filter(models.Message.id == message_id).first() + msg = db.query(models.Message).filter( + models.Message.id == message_id).first() if msg is None or msg.sender_id != user_id: continue try: @@ -186,7 +197,8 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db: "detail": "message_id must be int", }) continue - msg = db.query(models.Message).filter(models.Message.id == message_id).first() + msg = db.query(models.Message).filter( + models.Message.id == message_id).first() if msg is None or msg.sender_id != user_id: continue receiver_id = msg.receiver_id @@ -210,7 +222,8 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db: except (TypeError, ValueError): continue - msg = db.query(models.Message).filter(models.Message.id == message_id).first() + msg = db.query(models.Message).filter( + models.Message.id == message_id).first() if msg is None: continue @@ -237,10 +250,15 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db: pass finally: manager.disconnect(user_id) + db.query(models.User).filter(models.User.id == user_id).update( + {"last_online": datetime.now()}) + db.commit() + print("ОТКЛЮЧЕНИЕ") def send_fcm_notification(token, user_id, username, public_key, encrypted_text, timestamp): - print(f"DEBUG: Отправляем FCM уведомление пользователю {user_id} с токеном {token}") + print( + f"DEBUG: Отправляем FCM уведомление пользователю {user_id} с токеном {token}") message = messaging.Message( data={ "type": "enc_message", diff --git a/srv/main.py b/srv/main.py index eae1369..2f680b1 100644 --- a/srv/main.py +++ b/srv/main.py @@ -10,7 +10,7 @@ app = FastAPI() app.include_router(auth.authRouter) app.include_router(users.usersRouter) app.include_router(messages.messagesRouter) -app.include_router(media.mediaRouter) +#app.include_router(media.mediaRouter) app.include_router(wsRouter) app.add_middleware(