21-06-2026+22-20
This commit is contained in:
parent
ba1ed1032d
commit
4363fdf699
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() ?? '',
|
||||
|
|
|
|||
|
|
@ -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!,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in New Issue