From 4363fdf6995085345ec8a1433b8f011552362b97 Mon Sep 17 00:00:00 2001 From: Artur Date: Sun, 21 Jun 2026 22:20:43 +0500 Subject: [PATCH] 21-06-2026+22-20 --- lib/data/datasources/local_db_service.dart | 28 ----------- lib/domain/services/api_service.dart | 2 +- lib/domain/services/crypto_service.dart | 4 ++ lib/logic/auth_provider.dart | 47 +++++++++++++++++-- lib/logic/contact_provider.dart | 27 +++++++++++ lib/presentation/screens/chat_screen.dart | 30 ++++++++---- lib/presentation/screens/contacts_screen.dart | 27 +++++++++-- lib/presentation/screens/splash_screen.dart | 12 +++-- lib/presentation/widgets/message_bubble.dart | 2 - srv/app/api/endpoints/users.py | 3 ++ 10 files changed, 129 insertions(+), 53 deletions(-) diff --git a/lib/data/datasources/local_db_service.dart b/lib/data/datasources/local_db_service.dart index 30d237c..be3089e 100644 --- a/lib/data/datasources/local_db_service.dart +++ b/lib/data/datasources/local_db_service.dart @@ -1,10 +1,6 @@ -import 'dart:io'; - import 'package:chepuhagram/data/models/message_model.dart'; import 'package:drift/drift.dart'; import 'package:drift_sqflite/drift_sqflite.dart'; -import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; part 'local_db_service.g.dart'; @@ -72,15 +68,6 @@ class LocalDbService extends _$LocalDbService { Future saveMessages(List messageList) async { if (messageList.isEmpty) return; - final List incomingIds = messageList - .map( - (msg) => (msg is MessageModel) - ? msg.id - : (msg['id'] == null ? null : int.tryParse(msg['id'].toString())), - ) - .whereType() - .toList(); - // Преобразуем входящие данные в компаньоны заранее final companions = messageList.map((msg) { final int? id; @@ -153,21 +140,6 @@ class LocalDbService extends _$LocalDbService { // Выполняем все операции в рамках ОДНОЙ транзакции БД await transaction(() async { - if (incomingIds.isNotEmpty) { - // ВНИМАНИЕ: Ограничьте удаление только текущим чатом, - // иначе эта строка очистит сообщения из всех остальных диалогов! - final first = companions.first; - await (delete(messages)..where( - (tbl) => - ((tbl.senderId.equals(first.senderId.value) & - tbl.receiverId.equals(first.receiverId.value)) | - (tbl.senderId.equals(first.receiverId.value) & - tbl.receiverId.equals(first.senderId.value))) & - tbl.id.isNotIn(incomingIds), - )) - .go(); - } - // Быстрая пакетная вставка/обновление await batch((b) { b.insertAll( diff --git a/lib/domain/services/api_service.dart b/lib/domain/services/api_service.dart index c246a59..e9ccc8c 100644 --- a/lib/domain/services/api_service.dart +++ b/lib/domain/services/api_service.dart @@ -22,7 +22,7 @@ class ApiService extends ChangeNotifier { try { // Подставляй свой эндпоинт, например: /users/by-username/ final response = await Dio().get( - '${AppConstants.baseUrl}//users/by-username/$username', + '${AppConstants.baseUrl}/users/by-username/$username', ); if (response.statusCode == 200 && response.data != null) { diff --git a/lib/domain/services/crypto_service.dart b/lib/domain/services/crypto_service.dart index a711a6d..94a3a01 100644 --- a/lib/domain/services/crypto_service.dart +++ b/lib/domain/services/crypto_service.dart @@ -19,6 +19,10 @@ class CryptoService { _memoryKeysCache[contactId] = key; } + static void clearCache() { + _memoryKeysCache.clear(); + } + Future> initAccountSecurity(String masterPassword) async { // Генерируем пару X25519 ключей final keyPair = await algorithm.newKeyPair(); diff --git a/lib/logic/auth_provider.dart b/lib/logic/auth_provider.dart index b02fa15..a9778f9 100644 --- a/lib/logic/auth_provider.dart +++ b/lib/logic/auth_provider.dart @@ -1,5 +1,6 @@ import 'package:chepuhagram/data/datasources/local_db_service.dart'; import 'package:flutter/material.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; @@ -9,12 +10,11 @@ import 'package:http/http.dart' as http; import 'package:chepuhagram/domain/services/api_service.dart'; import 'package:chepuhagram/data/datasources/ws_client.dart'; import 'package:chepuhagram/domain/services/crypto_service.dart'; +import 'package:chepuhagram/logic/contact_provider.dart'; import 'package:provider/provider.dart'; import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; import 'package:chepuhagram/data/models/session_model.dart'; import 'package:chepuhagram/presentation/screens/splash_screen.dart'; -import 'package:flutter/services.dart'; import 'package:chepuhagram/main.dart'; class AuthProvider extends ChangeNotifier { @@ -126,6 +126,15 @@ class AuthProvider extends ChangeNotifier { final mode = await _storage.read(key: 'theme_mode'); final color = await _storage.read(key: 'accent_color'); await _storage.deleteAll(); + CryptoService.clearCache(); + final context = navigatorKey.currentContext; + if (context != null) { + try { + Provider.of(context, listen: false).clearCache(); + } catch (e) { + print("Error clearing contact provider cache: $e"); + } + } final prefs = await SharedPreferences.getInstance(); await prefs.clear(); await LocalDbService().clearDatabase(); @@ -149,13 +158,24 @@ class AuthProvider extends ChangeNotifier { // Поскольку у вас Drift открывает SqfliteQueryExecutor (local_db_service.dart), // самый надежный способ — физически удалить файл базы данных чепухаграма. try { - final docDir = - await getApplicationSupportDirectory(); // Или папка, где лежит БД у вас в local_db_service.dart - final dbFile = File(p.join(docDir.path, 'chepuhagram.db')); + final dbFolder = await databaseFactory.getDatabasesPath(); + final dbFile = File(p.join(dbFolder, 'chat_app.db')); if (await dbFile.exists()) { await dbFile.delete(); print("БАЗА ДАННЫХ УСПЕШНО УНИЧТОЖЕНА В ЦЕЛЯХ БЕЗОПАСНОСТИ"); } + final journalFile = File(p.join(dbFolder, 'chat_app.db-journal')); + if (await journalFile.exists()) { + await journalFile.delete(); + } + final walFile = File(p.join(dbFolder, 'chat_app.db-wal')); + if (await walFile.exists()) { + await walFile.delete(); + } + final shmFile = File(p.join(dbFolder, 'chat_app.db-shm')); + if (await shmFile.exists()) { + await shmFile.delete(); + } } catch (e) { print("Ошибка удаления файла БД: $e"); } @@ -470,6 +490,23 @@ class AuthProvider extends ChangeNotifier { // Метод для начала с чистого листа (новые ключи) Future resetKeys() async { await _storage.delete(key: 'private_key'); + try { + final allKeys = await _storage.readAll(); + for (final key in allKeys.keys) { + if (key.startsWith('contact_shared_key_') || key.startsWith('contact_public_key_')) { + await _storage.delete(key: key); + } + } + } catch (e) { + print("Error clearing cached contact keys in secure storage: $e"); + } + CryptoService.clearCache(); + final context = navigatorKey.currentContext; + if (context != null) { + try { + Provider.of(context, listen: false).clearCache(); + } catch (_) {} + } _needsKeyRecovery = false; notifyListeners(); } diff --git a/lib/logic/contact_provider.dart b/lib/logic/contact_provider.dart index b72856d..e65b118 100644 --- a/lib/logic/contact_provider.dart +++ b/lib/logic/contact_provider.dart @@ -28,6 +28,12 @@ class ContactProvider extends ChangeNotifier { void setSharedKey(int contactId, SecretKey key) { _sharedKeysCache[contactId] = key; + notifyListeners(); + } + + void clearCache() { + _sharedKeysCache.clear(); + notifyListeners(); } void setCurrentUserId(int? id) { @@ -184,6 +190,20 @@ class ContactProvider extends ChangeNotifier { ); _sortContacts(); notifyListeners(); + } else { + final newContact = updatedContact.copyWith( + lastMessage: lastMessage, + lastMessageTime: lastMessageTime, + isLastMsgDecrypted: isLastMsgDecrypted ?? false, + unreadCount: unreadCount ?? 0, + firstUnreadMessageId: firstUnreadMessageId, + ); + _contacts.add(newContact); + print( + "Новый контакт ${newContact.name} ${newContact.surname} добавлен в список чатов", + ); + _sortContacts(); + notifyListeners(); } } catch (e) { print("Error updating contact: $e"); @@ -248,6 +268,13 @@ class ContactProvider extends ChangeNotifier { ); _sortContacts(); notifyListeners(); + } else { + await updateContact( + contactId, + lastMessage: lastMessage, + lastMessageTime: lastMessageTime, + isLastMsgDecrypted: isLastMsgDecrypted, + ); } } catch (e) { print("Error updating contact last message: $e"); diff --git a/lib/presentation/screens/chat_screen.dart b/lib/presentation/screens/chat_screen.dart index 06c5319..a512c37 100644 --- a/lib/presentation/screens/chat_screen.dart +++ b/lib/presentation/screens/chat_screen.dart @@ -1,4 +1,4 @@ -import 'dart:async'; +import 'dart:async'; import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; @@ -533,10 +533,12 @@ class _ChatScreenState extends State with RouteAware { setState(() { _isOnline = data['online'] ?? false; - if (data['last_online'] != null) - _lastOnline = DateTime.parse(data['last_online']).add(offset); - else + if (data['last_online'] != null) { + final parsed = DateTime.tryParse(data['last_online']); + _lastOnline = parsed != null ? parsed.add(offset) : null; + } else { _lastOnline = null; + } }); } catch (e) { print(e); @@ -1583,6 +1585,10 @@ class _ChatScreenState extends State with RouteAware { context, listen: false, ).sendMessage({'type': 'delete_message', 'message_id': id}); + } else if (msg.tempId != null) { + try { + await _localDbService.deleteMessage(msg.tempId!); + } catch (_) {} } } @@ -3017,9 +3023,10 @@ class _ChatScreenState extends State with RouteAware { replyToId: data['reply_to_id'] != null ? int.tryParse(data['reply_to_id'].toString()) : null, - replyToText: decryptedReplyToText ?? null, + replyToText: decryptedReplyToText, ), ); + messages.sort((a, b) => a.createdAt.compareTo(b.createdAt)); }); } return; @@ -3496,7 +3503,9 @@ class _ChatScreenState extends State with RouteAware { } else { // ЕСЛИ ВСЁ ПРОЧИТАНО: WidgetsBinding.instance.addPostFrameCallback((_) { - _itemScrollController.jumpTo(index: 0); + if (messages.isNotEmpty && _itemScrollController.isAttached) { + _itemScrollController.jumpTo(index: 0); + } if (mounted) { final contactProvider = context.read(); contactProvider.updateContact( @@ -3721,7 +3730,9 @@ class _ChatScreenState extends State with RouteAware { // Если пользователь не был в самом низу, удерживаем его текущий видимый индекс if (firstVisibleIndex != null && firstVisibleIndex > 0) { WidgetsBinding.instance.addPostFrameCallback((_) { - _itemScrollController.jumpTo(index: firstVisibleIndex, alignment: 0); + if (_itemScrollController.isAttached) { + _itemScrollController.jumpTo(index: firstVisibleIndex, alignment: 0); + } }); } } catch (e) { @@ -4284,11 +4295,12 @@ class _ChatScreenState extends State with RouteAware { } Future _scrollToBottom() async { + if (messages.isEmpty || !_itemScrollController.isAttached) return; _itemScrollController.jumpTo(index: 0, alignment: 0); } Future _scrollToMessage(int? messageId) async { - if (messageId == null) return; + if (messageId == null || !_itemScrollController.isAttached) return; // 1. Ищем индекс в массиве final int msgIndex = messages.indexWhere((m) => m.id == messageId); @@ -4448,7 +4460,7 @@ class _ChatScreenState extends State with RouteAware { await Future.delayed(const Duration(milliseconds: 2000)); final index = messages.indexWhere((m) => m.id == anchorId); - if (index != -1) { + if (index != -1 && _itemScrollController.isAttached) { _itemScrollController.jumpTo(index: index, alignment: 0.5); } diff --git a/lib/presentation/screens/contacts_screen.dart b/lib/presentation/screens/contacts_screen.dart index 14e02db..922b209 100644 --- a/lib/presentation/screens/contacts_screen.dart +++ b/lib/presentation/screens/contacts_screen.dart @@ -1066,7 +1066,6 @@ class _ContactsScreenState extends State with RouteAware { ), ), child: Text( - // На Windows кнопка всегда предлагает "Обновить" _isDownloading ? "Отмена" : "Обновить", style: const TextStyle( color: Colors.white, @@ -1584,10 +1583,17 @@ class _ContactsScreenState extends State with RouteAware { // Обрабатываем сообщение ТОЛЬКО если оно от другого чата if (senderId != null && senderId != currentActiveChatContactId) { - final contact = contactProvider.contacts + var contact = contactProvider.contacts .where((c) => c.id == senderId) .firstOrNull; + if (contact == null) { + await contactProvider.updateContact(senderId); + contact = contactProvider.contacts + .where((c) => c.id == senderId) + .firstOrNull; + } + if (contact != null) { final currentUnread = contact.unreadCount; final msgId = int.tryParse(data['id']?.toString() ?? ''); @@ -1742,10 +1748,17 @@ class _ContactsScreenState extends State with RouteAware { if (data['type'] == 'message_sent') { final receiverId = int.tryParse(data['receiver_id']?.toString() ?? ''); if (receiverId != null) { - final contact = contactProvider.contacts + var contact = contactProvider.contacts .where((c) => c.id == receiverId) .firstOrNull; + if (contact == null) { + await contactProvider.updateContact(receiverId); + contact = contactProvider.contacts + .where((c) => c.id == receiverId) + .firstOrNull; + } + if (contact != null) { final messageType = MessageModel.parseMessageType( data['message_type']?.toString() ?? 'text', @@ -1853,9 +1866,15 @@ class _ContactsScreenState extends State with RouteAware { final messageId = int.tryParse(data['message_id']?.toString() ?? ''); final senderId = int.tryParse(data['sender_id']?.toString() ?? ''); if (messageId != null && senderId != null) { - final contact = contactProvider.contacts + var contact = contactProvider.contacts .where((c) => c.id == senderId) .firstOrNull; + if (contact == null) { + await contactProvider.updateContact(senderId); + contact = contactProvider.contacts + .where((c) => c.id == senderId) + .firstOrNull; + } if (contact != null) { final editedAt = DateTime.tryParse( data['edited_at']?.toString() ?? '', diff --git a/lib/presentation/screens/splash_screen.dart b/lib/presentation/screens/splash_screen.dart index 1257b2a..9dc6406 100644 --- a/lib/presentation/screens/splash_screen.dart +++ b/lib/presentation/screens/splash_screen.dart @@ -151,13 +151,17 @@ class _SplashScreenState extends State final myPrivKeyBase64 = await cryptoService.getPrivateKey(); if (myPrivKeyBase64 != null) { + final String privKeyFingerprint = myPrivKeyBase64.replaceAll(RegExp(r'[^a-zA-Z0-9]'), ''); + final String fingerprint = privKeyFingerprint.substring( + 0, privKeyFingerprint.length > 16 ? 16 : privKeyFingerprint.length); + // Проходим по каждому контакту строго ПО ОЧЕРЕДИ for (var c in contactProvider.contacts) { final savedKeyHex = await storage.read( - key: '$_contactSharedKey${c.id}', + key: '${_contactSharedKey}${fingerprint}_${c.id}', ); final savedPubKey = await storage.read( - key: '$_contactPublicKey${c.id}', + key: '${_contactPublicKey}${fingerprint}_${c.id}', ); if (savedKeyHex != null && savedPubKey == c.publicKey) { @@ -184,11 +188,11 @@ class _SplashScreenState extends State // Опускаем await для записи на диск, чтобы медленная файловая система не тормозила расчет следующего ключа. secretKey.extractBytes().then((bytes) { storage.write( - key: '$_contactSharedKey${c.id}', + key: '${_contactSharedKey}${fingerprint}_${c.id}', value: base64Encode(bytes), ); storage.write( - key: '$_contactPublicKey${c.id}', + key: '${_contactPublicKey}${fingerprint}_${c.id}', value: c.publicKey!, ); }); diff --git a/lib/presentation/widgets/message_bubble.dart b/lib/presentation/widgets/message_bubble.dart index 7251d82..4f5b106 100644 --- a/lib/presentation/widgets/message_bubble.dart +++ b/lib/presentation/widgets/message_bubble.dart @@ -757,8 +757,6 @@ class _MessageBubbleState extends State { decoration: BoxDecoration( color: widget.message.messageType == MessageType.videoNote ? Colors.transparent - : (isUnread && kDebugMode) - ? Colors.red : (isMe ? Theme.of(context).colorScheme.brightness == Brightness.dark diff --git a/srv/app/api/endpoints/users.py b/srv/app/api/endpoints/users.py index 5a661c1..6af3aba 100644 --- a/srv/app/api/endpoints/users.py +++ b/srv/app/api/endpoints/users.py @@ -39,6 +39,9 @@ def _build_user_search_filter(query: str): func.lower(models.User.username).like(normalized), func.lower(models.User.first_name).like(normalized), func.lower(models.User.last_name).like(normalized), + models.User.phone.like(normalized), + func.lower(models.User.first_name + " " + func.coalesce(models.User.last_name, '')).like(normalized), + func.lower(func.coalesce(models.User.last_name, '') + " " + models.User.first_name).like(normalized), ) async def _delete_old_avatar_file(file_id: str, db: AsyncSession):