Время учитывает часовой пояс, опитимизирован запрос контакров

This commit is contained in:
Artur 2026-05-01 23:48:44 +05:00
parent 1a36cbccd3
commit 15af40fc64
15 changed files with 364 additions and 99 deletions

View File

@ -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,
);
}
}

View File

@ -10,6 +10,12 @@ class ContactRepository {
Future<List<Contact>> 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<dynamic> data = jsonDecode(utf8.decode(response.bodyBytes));
return data.map((json) => Contact.fromJson(json)).toList();
List<Contact> 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<List<Contact>> 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<dynamic> data = jsonDecode(utf8.decode(response.bodyBytes));
return data.map((json) => Contact.fromJson(json)).toList();
List<Contact> 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');
}

View File

@ -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<String> 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<List<Contact>> bulkDecryptContacts(
Map<String, dynamic> data,
) async {
final List<Contact> contacts = data['contacts'];
final String privKey = data['privKey'];
final Map<int, SecretKey> cache = data['cache'];
final x25519 = X25519();
final aesGcm = AesGcm.with256bits();
final List<Contact> 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<Map<int, List<int>>> computeSharedKeysTask(
Map<String, dynamic> params,
) async {
final Map<int, String> isolateKeysMap = params['keysMap'];
final String isolatePrivKey = params['privKey'];
final x25519 = X25519();
final Map<int, List<int>> 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<int>, String)?> encryptImage(
List<int> fileBytes,
SecretKey sharedKey,

View File

@ -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<int, SecretKey> _sharedKeysCache = {};
List<Contact> _contacts = [];
List<Contact> _allContacts = [];
bool _isLoading = false;
@ -19,6 +26,11 @@ class ContactProvider extends ChangeNotifier {
List<Contact> get allContacts => _allContacts;
bool get isLoading => _isLoading;
String? get error => _error;
Map<int, SecretKey> 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<void> loadContacts() async {
Future<void> 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<void> 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<void> _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<Contact>.from(_contacts);
final cacheCopy = Map<int, SecretKey>.from(_sharedKeysCache);
final List<Contact> updated = List<Contact>.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");
}
}
}

View File

@ -22,15 +22,12 @@ final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
// Глобальная переменная для отслеживания текущего активного контакта в чате
int? currentActiveChatContactId;
// Глобальная переменная для хранения начального сообщения (при запуске из уведомления)
RemoteMessage? initialMessage;
// Ключ для SharedPreferences
const String _notificationLaunchKey = 'notification_launch_data';
// Защита от повторной обработки одного и того же payload при следующих запусках по иконке
const String _lastHandledNotificationLaunchPayloadKey =
'notification_last_handled_payload';
@ -49,9 +46,6 @@ Future<void> _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<void> _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<CryptoService>(),
),
),
],
child: const MyApp(),
),

View File

@ -494,6 +494,10 @@ class _ChatScreenState extends State<ChatScreen> {
}
_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<ChatScreen> {
}
void _handleIncomingMessage(Map<String, dynamic> 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<ChatScreen> {
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<ChatScreen> {
// Доставка онлайн (получатель был в сети)
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<ChatScreen> {
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<ChatScreen> {
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<ChatScreen> {
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<ChatScreen> {
}
Future<void> _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<ChatScreen> {
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<ChatScreen> {
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<ChatScreen> {
? 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<ChatScreen> {
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<ChatScreen> {
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<ChatScreen> {
? 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<ChatScreen> {
Provider.of<SocketService>(context, listen: false).sendReadReceipt(id);
_sentReadReceipts.add(id);
}
await _localDbService.deleteChatHistory(widget.contact.id, myId);
} catch (e) {
print("Ошибка загрузки истории: $e");
if (!mounted) return;

View File

@ -55,21 +55,28 @@ class _ContactsScreenState extends State<ContactsScreen> 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<void> _initContacts() async {
final contactProvider = context.read<ContactProvider>();
// Ждем завершения загрузки контактов
await contactProvider.loadContacts();
print('Contacts loaded, checking targetChatId: ${widget.targetChatId}');
// Дальнейшая логика выполнится только после того, как loadContacts завершится
if (widget.targetChatId != null) {
_navigateToTargetChat();
} else {
_checkSavedNotificationTarget();
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();

View File

@ -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<SplashScreen> {
// Ключ для 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<SplashScreen> {
// Проверяем, было ли приложение запущено из уведомления
int? targetChatId =
_targetChatId; // Сначала проверяем из onMessageOpenedApp
// Если не установлено, проверяем SharedPreferences
if (targetChatId == null) {
final prefs = await SharedPreferences.getInstance();
final savedData = prefs.getString(_notificationLaunchKey);
try {
final contactProvider = context.read<ContactProvider>();
contactProvider.setCurrentUserId(authProvider.currentUserId);
await contactProvider.loadContacts(enrichContacts: false);
final myPrivKeyBase64 = await context
.read<CryptoService>()
.getPrivateKey();
if (myPrivKeyBase64 != null) {
final Map<int, String> 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<String, dynamic>;
@ -178,7 +235,7 @@ class _SplashScreenState extends State<SplashScreen> {
try {
final contactProvider = context.read<ContactProvider>();
contactProvider.setCurrentUserId(authProvider.currentUserId);
await contactProvider.loadContacts();
await contactProvider.loadContacts(enrichContacts: false);
final contact = contactProvider.contacts.firstWhere(
(c) => c.id == targetChatId,

View File

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

View File

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

View File

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

View File

@ -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),
}
@ -289,4 +291,11 @@ 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

View File

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

View File

@ -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,6 +41,10 @@ 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("ОЖИДАНИЕ СООБЩЕНИЙ")
@ -47,9 +52,14 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db:
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",

View File

@ -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(