Время учитывает часовой пояс, опитимизирован запрос контакров
This commit is contained in:
parent
1a36cbccd3
commit
15af40fc64
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue