21-06-2026+22-20

This commit is contained in:
Artur 2026-06-21 22:20:43 +05:00
parent ba1ed1032d
commit 4363fdf699
10 changed files with 129 additions and 53 deletions

View File

@ -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<void> saveMessages(List<dynamic> messageList) async {
if (messageList.isEmpty) return;
final List<int> incomingIds = messageList
.map<int?>(
(msg) => (msg is MessageModel)
? msg.id
: (msg['id'] == null ? null : int.tryParse(msg['id'].toString())),
)
.whereType<int>()
.toList();
// Преобразуем входящие данные в компаньоны заранее
final companions = messageList.map<MessagesCompanion>((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(

View File

@ -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) {

View File

@ -19,6 +19,10 @@ class CryptoService {
_memoryKeysCache[contactId] = key;
}
static void clearCache() {
_memoryKeysCache.clear();
}
Future<Map<String, String>> initAccountSecurity(String masterPassword) async {
// Генерируем пару X25519 ключей
final keyPair = await algorithm.newKeyPair();

View File

@ -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<ContactProvider>(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<void> 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<ContactProvider>(context, listen: false).clearCache();
} catch (_) {}
}
_needsKeyRecovery = false;
notifyListeners();
}

View File

@ -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");

View File

@ -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<ChatScreen> 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<ChatScreen> 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<ChatScreen> 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<ChatScreen> 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>();
contactProvider.updateContact(
@ -3721,7 +3730,9 @@ class _ChatScreenState extends State<ChatScreen> 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<ChatScreen> with RouteAware {
}
Future<void> _scrollToBottom() async {
if (messages.isEmpty || !_itemScrollController.isAttached) return;
_itemScrollController.jumpTo(index: 0, alignment: 0);
}
Future<void> _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<ChatScreen> 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);
}

View File

@ -1066,7 +1066,6 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
),
),
child: Text(
// На Windows кнопка всегда предлагает "Обновить"
_isDownloading ? "Отмена" : "Обновить",
style: const TextStyle(
color: Colors.white,
@ -1584,10 +1583,17 @@ class _ContactsScreenState extends State<ContactsScreen> 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<ContactsScreen> 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<ContactsScreen> 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() ?? '',

View File

@ -151,13 +151,17 @@ class _SplashScreenState extends State<SplashScreen>
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<SplashScreen>
// Опускаем 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!,
);
});

View File

@ -757,8 +757,6 @@ class _MessageBubbleState extends State<MessageBubble> {
decoration: BoxDecoration(
color: widget.message.messageType == MessageType.videoNote
? Colors.transparent
: (isUnread && kDebugMode)
? Colors.red
: (isMe
? Theme.of(context).colorScheme.brightness ==
Brightness.dark

View File

@ -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):