Время учитывает часовой пояс, опитимизирован запрос контакров
This commit is contained in:
parent
1a36cbccd3
commit
15af40fc64
|
|
@ -9,6 +9,7 @@ class Contact {
|
||||||
final bool isOnline;
|
final bool isOnline;
|
||||||
final int unreadCount;
|
final int unreadCount;
|
||||||
final String? publicKey;
|
final String? publicKey;
|
||||||
|
final bool isLastMsgDecrypted;
|
||||||
|
|
||||||
Contact({
|
Contact({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
|
@ -21,6 +22,7 @@ class Contact {
|
||||||
this.isOnline = false,
|
this.isOnline = false,
|
||||||
this.unreadCount = 0,
|
this.unreadCount = 0,
|
||||||
this.publicKey,
|
this.publicKey,
|
||||||
|
this.isLastMsgDecrypted = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
Contact copyWith({
|
Contact copyWith({
|
||||||
|
|
@ -34,6 +36,7 @@ class Contact {
|
||||||
bool? isOnline,
|
bool? isOnline,
|
||||||
int? unreadCount,
|
int? unreadCount,
|
||||||
String? publicKey,
|
String? publicKey,
|
||||||
|
bool? isLastMsgDecrypted,
|
||||||
}) {
|
}) {
|
||||||
return Contact(
|
return Contact(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
|
|
@ -46,6 +49,7 @@ class Contact {
|
||||||
isOnline: isOnline ?? this.isOnline,
|
isOnline: isOnline ?? this.isOnline,
|
||||||
unreadCount: unreadCount ?? this.unreadCount,
|
unreadCount: unreadCount ?? this.unreadCount,
|
||||||
publicKey: publicKey ?? this.publicKey,
|
publicKey: publicKey ?? this.publicKey,
|
||||||
|
isLastMsgDecrypted: isLastMsgDecrypted ?? this.isLastMsgDecrypted,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,6 +72,7 @@ class Contact {
|
||||||
isOnline: (json['is_online'] ?? json['isOnline']) == true,
|
isOnline: (json['is_online'] ?? json['isOnline']) == true,
|
||||||
unreadCount: int.tryParse((json['unread_count'] ?? json['unreadCount'] ?? 0).toString()) ?? 0,
|
unreadCount: int.tryParse((json['unread_count'] ?? json['unreadCount'] ?? 0).toString()) ?? 0,
|
||||||
publicKey: json['public_key'],
|
publicKey: json['public_key'],
|
||||||
|
isLastMsgDecrypted: json['is_last_msg_decrypted'] ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,12 @@ class ContactRepository {
|
||||||
|
|
||||||
Future<List<Contact>> fetchChatContacts() async {
|
Future<List<Contact>> fetchChatContacts() async {
|
||||||
final token = await _apiService.getAccessToken();
|
final token = await _apiService.getAccessToken();
|
||||||
|
|
||||||
|
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
|
||||||
|
Duration offset = now.timeZoneOffset;
|
||||||
|
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
throw Exception('No access token');
|
throw Exception('No access token');
|
||||||
}
|
}
|
||||||
|
|
@ -24,7 +30,14 @@ class ContactRepository {
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final List<dynamic> data = jsonDecode(utf8.decode(response.bodyBytes));
|
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 {
|
} else {
|
||||||
throw Exception('Failed to load contacts');
|
throw Exception('Failed to load contacts');
|
||||||
}
|
}
|
||||||
|
|
@ -32,6 +45,10 @@ class ContactRepository {
|
||||||
|
|
||||||
Future<List<Contact>> fetchAllUsers() async {
|
Future<List<Contact>> fetchAllUsers() async {
|
||||||
final token = await _apiService.getAccessToken();
|
final token = await _apiService.getAccessToken();
|
||||||
|
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
Duration offset = now.timeZoneOffset;
|
||||||
|
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
throw Exception('No access token');
|
throw Exception('No access token');
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +63,14 @@ class ContactRepository {
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final List<dynamic> data = jsonDecode(utf8.decode(response.bodyBytes));
|
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 {
|
} else {
|
||||||
throw Exception('Failed to load contacts');
|
throw Exception('Failed to load contacts');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'dart:typed_data';
|
||||||
import 'package:cryptography/cryptography.dart';
|
import 'package:cryptography/cryptography.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:chepuhagram/data/models/contact_model.dart';
|
||||||
|
|
||||||
class CryptoService {
|
class CryptoService {
|
||||||
final _storage = const FlutterSecureStorage();
|
final _storage = const FlutterSecureStorage();
|
||||||
|
|
@ -123,6 +124,117 @@ class CryptoService {
|
||||||
return base64Encode(nonce + encrypted.mac.bytes + encrypted.cipherText);
|
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(
|
Future<(List<int>, String)?> encryptImage(
|
||||||
List<int> fileBytes,
|
List<int> fileBytes,
|
||||||
SecretKey sharedKey,
|
SecretKey sharedKey,
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,18 @@ import '/data/models/contact_model.dart';
|
||||||
import '/data/repositories/contact_repository.dart';
|
import '/data/repositories/contact_repository.dart';
|
||||||
import '/data/datasources/local_db_service.dart';
|
import '/data/datasources/local_db_service.dart';
|
||||||
import '/domain/services/crypto_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 {
|
class ContactProvider extends ChangeNotifier {
|
||||||
final ContactRepository _repository = ContactRepository();
|
final ContactRepository _repository = ContactRepository();
|
||||||
final LocalDbService _localDbService = LocalDbService();
|
final LocalDbService _localDbService = LocalDbService();
|
||||||
final CryptoService _cryptoService = CryptoService();
|
final CryptoService _cryptoService;
|
||||||
|
|
||||||
|
ContactProvider(this._cryptoService);
|
||||||
|
final Map<int, SecretKey> _sharedKeysCache = {};
|
||||||
List<Contact> _contacts = [];
|
List<Contact> _contacts = [];
|
||||||
List<Contact> _allContacts = [];
|
List<Contact> _allContacts = [];
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
|
@ -19,6 +26,11 @@ class ContactProvider extends ChangeNotifier {
|
||||||
List<Contact> get allContacts => _allContacts;
|
List<Contact> get allContacts => _allContacts;
|
||||||
bool get isLoading => _isLoading;
|
bool get isLoading => _isLoading;
|
||||||
String? get error => _error;
|
String? get error => _error;
|
||||||
|
Map<int, SecretKey> get sharedKeysCache => _sharedKeysCache;
|
||||||
|
|
||||||
|
void setSharedKey(int contactId, SecretKey key) {
|
||||||
|
_sharedKeysCache[contactId] = key;
|
||||||
|
}
|
||||||
|
|
||||||
void setCurrentUserId(int? id) {
|
void setCurrentUserId(int? id) {
|
||||||
_currentUserId = id;
|
_currentUserId = id;
|
||||||
|
|
@ -29,7 +41,7 @@ class ContactProvider extends ChangeNotifier {
|
||||||
return _currentUserId;
|
return _currentUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadContacts() async {
|
Future<void> loadContacts({bool enrichContacts = true}) async {
|
||||||
if (_isFirstLoad) {
|
if (_isFirstLoad) {
|
||||||
_isFirstLoad = false;
|
_isFirstLoad = false;
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
|
|
@ -39,21 +51,23 @@ class ContactProvider extends ChangeNotifier {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final allContacts = await _repository.fetchChatContacts();
|
final allContacts = await _repository.fetchChatContacts();
|
||||||
// Фильтруем: исключаем себя (для основного списка - только чаты)
|
final userIdCopy = _currentUserId;
|
||||||
_contacts = allContacts
|
_contacts = await Isolate.run(() {
|
||||||
.where((contact) => contact.id != _currentUserId)
|
return allContacts
|
||||||
.toList();
|
.where((contact) => contact.id != userIdCopy)
|
||||||
|
.toList();
|
||||||
|
});
|
||||||
_allContacts = _contacts;
|
_allContacts = _contacts;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
// Обогащаем превью последним сообщением из локальной БД, не блокируя UI.
|
if (enrichContacts) {
|
||||||
_enrichContactsWithLastMessages();
|
await _enrichContactsWithLastMessages();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_error = e.toString();
|
_error = e.toString();
|
||||||
|
print("❌ ОШИБКА ПРИ ЗАГРУЗКЕ КОНТАКТОВ: $_error");
|
||||||
} finally {
|
} finally {
|
||||||
// Если ошибка — выходим из состояния загрузки тут.
|
|
||||||
// Если всё ок — `_isLoading` уже сброшен выше, чтобы показать список быстрее.
|
|
||||||
if (_error != null) {
|
if (_error != null) {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +75,6 @@ class ContactProvider extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Метод для получения всех контактов (исключая себя) для нового чата
|
|
||||||
Future<void> loadAllContactsForNewChat() async {
|
Future<void> loadAllContactsForNewChat() async {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_error = null;
|
_error = null;
|
||||||
|
|
@ -69,7 +82,6 @@ class ContactProvider extends ChangeNotifier {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final allContacts = await _repository.fetchAllUsers();
|
final allContacts = await _repository.fetchAllUsers();
|
||||||
// Фильтруем только исключение самого себя
|
|
||||||
_allContacts = allContacts
|
_allContacts = allContacts
|
||||||
.where((contact) => contact.id != _currentUserId)
|
.where((contact) => contact.id != _currentUserId)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
@ -82,42 +94,30 @@ class ContactProvider extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _enrichContactsWithLastMessages() async {
|
Future<void> _enrichContactsWithLastMessages() async {
|
||||||
print("Начинаем обогащать контакты последними сообщениями из локальной БД... Для текущего пользователя ID: $_currentUserId");
|
final myPrivKeyBase64 = await _cryptoService.getPrivateKey();
|
||||||
final myId = _currentUserId;
|
if (myPrivKeyBase64 == null) return;
|
||||||
if (myId == null) return;
|
|
||||||
print("Текущий пользователь ID: $myId");
|
|
||||||
|
|
||||||
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++) {
|
try {
|
||||||
final contact = updated[i];
|
final updatedContacts = await compute(
|
||||||
|
CryptoService.bulkDecryptContacts,
|
||||||
|
{
|
||||||
|
'contacts': contactsToProcess,
|
||||||
|
'privKey': myPrivKeyBase64,
|
||||||
|
'cache': cacheCopy,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// 1) Если сервер уже прислал lastMessage — попробуем расшифровать превью.
|
_contacts = updatedContacts;
|
||||||
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;
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
print("Ошибка дешифровки: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,15 +22,12 @@ final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
|
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
|
||||||
|
|
||||||
// Глобальная переменная для отслеживания текущего активного контакта в чате
|
|
||||||
int? currentActiveChatContactId;
|
int? currentActiveChatContactId;
|
||||||
|
|
||||||
// Глобальная переменная для хранения начального сообщения (при запуске из уведомления)
|
|
||||||
RemoteMessage? initialMessage;
|
RemoteMessage? initialMessage;
|
||||||
|
|
||||||
// Ключ для SharedPreferences
|
// Ключ для SharedPreferences
|
||||||
const String _notificationLaunchKey = 'notification_launch_data';
|
const String _notificationLaunchKey = 'notification_launch_data';
|
||||||
// Защита от повторной обработки одного и того же payload при следующих запусках по иконке
|
|
||||||
const String _lastHandledNotificationLaunchPayloadKey =
|
const String _lastHandledNotificationLaunchPayloadKey =
|
||||||
'notification_last_handled_payload';
|
'notification_last_handled_payload';
|
||||||
|
|
||||||
|
|
@ -49,9 +46,6 @@ Future<void> _onSelectNotification(
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final canonicalPayload = jsonEncode(data);
|
final canonicalPayload = jsonEncode(data);
|
||||||
|
|
||||||
// Важно: не сохраняем payload в SharedPreferences, если можем сразу перейти в чат.
|
|
||||||
// Иначе при следующем обычном запуске (по иконке) останется "хвост" и приложение
|
|
||||||
// будет снова автопереходить в чат.
|
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
final lastHandled = prefs.getString(
|
final lastHandled = prefs.getString(
|
||||||
_lastHandledNotificationLaunchPayloadKey,
|
_lastHandledNotificationLaunchPayloadKey,
|
||||||
|
|
@ -70,7 +64,6 @@ Future<void> _onSelectNotification(
|
||||||
await prefs.remove(_notificationLaunchKey);
|
await prefs.remove(_notificationLaunchKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to chat with this contact (if context is ready)
|
|
||||||
_navigateToChat(senderId);
|
_navigateToChat(senderId);
|
||||||
} else {
|
} else {
|
||||||
print(
|
print(
|
||||||
|
|
@ -135,8 +128,6 @@ void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await Firebase.initializeApp();
|
await Firebase.initializeApp();
|
||||||
|
|
||||||
// Проверяем, было ли приложение запущено из уведомления
|
|
||||||
// Добавляем небольшую задержку, чтобы Firebase полностью инициализировался
|
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
initialMessage = await FirebaseMessaging.instance.getInitialMessage();
|
initialMessage = await FirebaseMessaging.instance.getInitialMessage();
|
||||||
print('Initial message from main() after delay: $initialMessage');
|
print('Initial message from main() after delay: $initialMessage');
|
||||||
|
|
@ -226,10 +217,17 @@ void main() async {
|
||||||
runApp(
|
runApp(
|
||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
|
Provider(create: (_) => CryptoService()),
|
||||||
|
Provider(create: (_) => SocketService()),
|
||||||
ChangeNotifierProvider(create: (_) => AuthProvider()),
|
ChangeNotifierProvider(create: (_) => AuthProvider()),
|
||||||
ChangeNotifierProvider(create: (_) => ThemeProvider()),
|
ChangeNotifierProvider(create: (_) => ThemeProvider()),
|
||||||
ChangeNotifierProvider(create: (_) => ContactProvider()),
|
|
||||||
Provider(create: (_) => SocketService()),
|
Provider(create: (_) => SocketService()),
|
||||||
|
|
||||||
|
ChangeNotifierProvider(
|
||||||
|
create: (context) => ContactProvider(
|
||||||
|
context.read<CryptoService>(),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: const MyApp(),
|
child: const MyApp(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -494,6 +494,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
}
|
}
|
||||||
_replyTo = null;
|
_replyTo = null;
|
||||||
});
|
});
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) => ChatScreen(contact: targetContact,)),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|
@ -754,11 +758,16 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleIncomingMessage(Map<String, dynamic> data) async {
|
void _handleIncomingMessage(Map<String, dynamic> data) async {
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
|
||||||
|
Duration offset = now.timeZoneOffset;
|
||||||
// ACK от сервера: сообщение сохранено и получило server_id
|
// ACK от сервера: сообщение сохранено и получило server_id
|
||||||
if (data['type'] == 'message_sent') {
|
if (data['type'] == 'message_sent') {
|
||||||
final tempId = int.tryParse(data['temp_id']?.toString() ?? '');
|
final tempId = int.tryParse(data['temp_id']?.toString() ?? '');
|
||||||
final serverId = int.tryParse(data['server_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;
|
if (tempId == null) return;
|
||||||
|
|
||||||
|
|
@ -779,7 +788,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
if (data['type'] == 'message_delivered' && data.containsKey('temp_id')) {
|
if (data['type'] == 'message_delivered' && data.containsKey('temp_id')) {
|
||||||
final tempId = int.tryParse(data['temp_id']?.toString() ?? '');
|
final tempId = int.tryParse(data['temp_id']?.toString() ?? '');
|
||||||
final serverId = int.tryParse(data['server_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;
|
if (tempId == null) return;
|
||||||
|
|
||||||
|
|
@ -799,7 +810,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
// Доставка онлайн (получатель был в сети)
|
// Доставка онлайн (получатель был в сети)
|
||||||
if (data['type'] == 'message_delivered') {
|
if (data['type'] == 'message_delivered') {
|
||||||
final messageId = int.tryParse(data['message_id']?.toString() ?? '');
|
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 (messageId == null) return;
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
@ -821,7 +834,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
|
|
||||||
if (data['type'] == 'message_edited') {
|
if (data['type'] == 'message_edited') {
|
||||||
final messageId = int.tryParse(data['message_id']?.toString() ?? '');
|
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;
|
if (messageId == null) return;
|
||||||
|
|
||||||
final myPrivKey = await _cryptoService.getPrivateKey();
|
final myPrivKey = await _cryptoService.getPrivateKey();
|
||||||
|
|
@ -871,7 +886,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
if (data['type'] == 'message_read') {
|
if (data['type'] == 'message_read') {
|
||||||
final messageId = int.tryParse(data['message_id'].toString());
|
final messageId = int.tryParse(data['message_id'].toString());
|
||||||
if (messageId == null) return;
|
if (messageId == null) return;
|
||||||
final ts = DateTime.tryParse(data['timestamp']?.toString() ?? '');
|
var ts = DateTime.tryParse(
|
||||||
|
data['timestamp']?.toString() ?? '',
|
||||||
|
)?.add(offset);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -943,15 +960,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
isMe: false,
|
isMe: false,
|
||||||
senderId: senderId,
|
senderId: senderId,
|
||||||
receiverId: myId,
|
receiverId: myId,
|
||||||
createdAt: DateTime.parse(data['timestamp']),
|
createdAt: DateTime.parse(data['timestamp']).add(offset),
|
||||||
status: MessageStatus.delivered,
|
status: MessageStatus.delivered,
|
||||||
replyToId: data['reply_to_id'] == null
|
replyToId: data['reply_to_id'] == null
|
||||||
? null
|
? null
|
||||||
: int.tryParse(data['reply_to_id'].toString()),
|
: int.tryParse(data['reply_to_id'].toString()),
|
||||||
replyToText:
|
replyToText: data['reply_to_text'] != null
|
||||||
data['reply_to_text'] != null
|
? data['reply_to_text'].toString()
|
||||||
? data['reply_to_text'].toString()
|
: null,
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -968,6 +984,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadHistory() async {
|
Future<void> _loadHistory() async {
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
|
||||||
|
Duration offset = now.timeZoneOffset;
|
||||||
initialMessage = null; // Сбрасываем данные уведомления при загрузке ключа
|
initialMessage = null; // Сбрасываем данные уведомления при загрузке ключа
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.remove(_notificationLaunchKey);
|
await prefs.remove(_notificationLaunchKey);
|
||||||
|
|
@ -992,10 +1011,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
|
|
||||||
final deliveredAt = msg['delivered_at'] == null
|
final deliveredAt = msg['delivered_at'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.tryParse(msg['delivered_at'].toString());
|
: DateTime.tryParse(msg['delivered_at'].toString())?.add(offset);
|
||||||
final readAt = msg['read_at'] == null
|
final readAt = msg['read_at'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.tryParse(msg['read_at'].toString());
|
: DateTime.tryParse(msg['read_at'].toString())?.add(offset);
|
||||||
|
|
||||||
MessageStatus status = (msg['sender_id'] == myId)
|
MessageStatus status = (msg['sender_id'] == myId)
|
||||||
? MessageStatus.sent
|
? MessageStatus.sent
|
||||||
|
|
@ -1015,7 +1034,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
isMe: msg['sender_id'] == myId,
|
isMe: msg['sender_id'] == myId,
|
||||||
senderId: msg['sender_id'],
|
senderId: msg['sender_id'],
|
||||||
receiverId: msg['receiver_id'],
|
receiverId: msg['receiver_id'],
|
||||||
createdAt: DateTime.parse(msg['timestamp']),
|
createdAt: DateTime.parse(msg['timestamp']).add(offset),
|
||||||
status: status,
|
status: status,
|
||||||
replyToId: msg['reply_to_id'] == null
|
replyToId: msg['reply_to_id'] == null
|
||||||
? null
|
? null
|
||||||
|
|
@ -1024,7 +1043,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
? msg['reply_to_text'].toString()
|
? msg['reply_to_text'].toString()
|
||||||
: null,
|
: null,
|
||||||
editedAt: msg['edited_at'] != null
|
editedAt: msg['edited_at'] != null
|
||||||
? DateTime.tryParse(msg['edited_at'].toString())
|
? DateTime.tryParse(msg['edited_at'].toString())?.add(offset)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -1059,10 +1078,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
|
|
||||||
final deliveredAt = msg['delivered_at'] == null
|
final deliveredAt = msg['delivered_at'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.tryParse(msg['delivered_at'].toString());
|
: DateTime.tryParse(msg['delivered_at'].toString())?.add(offset);
|
||||||
final readAt = msg['read_at'] == null
|
final readAt = msg['read_at'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.tryParse(msg['read_at'].toString());
|
: DateTime.tryParse(msg['read_at'].toString())?.add(offset);
|
||||||
|
|
||||||
MessageStatus status = (msg['sender_id'] == myId)
|
MessageStatus status = (msg['sender_id'] == myId)
|
||||||
? MessageStatus.sent
|
? MessageStatus.sent
|
||||||
|
|
@ -1083,7 +1102,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
isMe: msg['sender_id'] == myId,
|
isMe: msg['sender_id'] == myId,
|
||||||
senderId: msg['sender_id'],
|
senderId: msg['sender_id'],
|
||||||
receiverId: msg['receiver_id'],
|
receiverId: msg['receiver_id'],
|
||||||
createdAt: DateTime.parse(msg['timestamp']),
|
createdAt: DateTime.parse(msg['timestamp']).add(offset),
|
||||||
status: status,
|
status: status,
|
||||||
replyToId: msg['reply_to_id'] == null
|
replyToId: msg['reply_to_id'] == null
|
||||||
? null
|
? null
|
||||||
|
|
@ -1092,12 +1111,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
? msg['reply_to_text'].toString()
|
? msg['reply_to_text'].toString()
|
||||||
: null,
|
: null,
|
||||||
editedAt: msg['edited_at'] != null
|
editedAt: msg['edited_at'] != null
|
||||||
? DateTime.tryParse(msg['edited_at'].toString())
|
? DateTime.tryParse(msg['edited_at'].toString())?.add(offset)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
await _localDbService.deleteChatHistory(widget.contact.id, myId);
|
||||||
await _localDbService.saveMessages(history);
|
await _localDbService.saveMessages(history);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Ошибка сохранения истории в локальную базу: $e");
|
print("Ошибка сохранения истории в локальную базу: $e");
|
||||||
|
|
@ -1119,7 +1139,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
Provider.of<SocketService>(context, listen: false).sendReadReceipt(id);
|
Provider.of<SocketService>(context, listen: false).sendReadReceipt(id);
|
||||||
_sentReadReceipts.add(id);
|
_sentReadReceipts.add(id);
|
||||||
}
|
}
|
||||||
await _localDbService.deleteChatHistory(widget.contact.id, myId);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Ошибка загрузки истории: $e");
|
print("Ошибка загрузки истории: $e");
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
|
||||||
|
|
@ -55,21 +55,28 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
|
||||||
'Setting current user ID in ContactProvider: ${authProvider.currentUserId}',
|
'Setting current user ID in ContactProvider: ${authProvider.currentUserId}',
|
||||||
);
|
);
|
||||||
contactProvider.setCurrentUserId(authProvider.currentUserId);
|
contactProvider.setCurrentUserId(authProvider.currentUserId);
|
||||||
contactProvider.loadContacts().then((_) {
|
_initContacts();
|
||||||
print('Contacts loaded, checking targetChatId: ${widget.targetChatId}');
|
|
||||||
// После загрузки контактов проверить, нужно ли перейти к чату
|
|
||||||
if (widget.targetChatId != null) {
|
|
||||||
_navigateToTargetChat();
|
|
||||||
} else {
|
|
||||||
_checkSavedNotificationTarget();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_checkAppUpdate();
|
_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
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
@ -15,6 +16,13 @@ import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:chepuhagram/main.dart';
|
import 'package:chepuhagram/main.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'dart:convert';
|
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 {
|
class SplashScreen extends StatefulWidget {
|
||||||
const SplashScreen({super.key});
|
const SplashScreen({super.key});
|
||||||
|
|
@ -29,6 +37,8 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||||
|
|
||||||
// Ключ для SharedPreferences
|
// Ключ для SharedPreferences
|
||||||
static const String _notificationLaunchKey = 'notification_launch_data';
|
static const String _notificationLaunchKey = 'notification_launch_data';
|
||||||
|
static const String _contactPublicKey = 'contact_public_key_';
|
||||||
|
static const String _contactSharedKey = 'contact_shared_key_';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -117,12 +127,59 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||||
// Проверяем, было ли приложение запущено из уведомления
|
// Проверяем, было ли приложение запущено из уведомления
|
||||||
int? targetChatId =
|
int? targetChatId =
|
||||||
_targetChatId; // Сначала проверяем из onMessageOpenedApp
|
_targetChatId; // Сначала проверяем из onMessageOpenedApp
|
||||||
|
|
||||||
// Если не установлено, проверяем SharedPreferences
|
|
||||||
if (targetChatId == null) {
|
if (targetChatId == null) {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final savedData = prefs.getString(_notificationLaunchKey);
|
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) {
|
if (savedData != null) {
|
||||||
try {
|
try {
|
||||||
final data = jsonDecode(savedData) as Map<String, dynamic>;
|
final data = jsonDecode(savedData) as Map<String, dynamic>;
|
||||||
|
|
@ -178,7 +235,7 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||||
try {
|
try {
|
||||||
final contactProvider = context.read<ContactProvider>();
|
final contactProvider = context.read<ContactProvider>();
|
||||||
contactProvider.setCurrentUserId(authProvider.currentUserId);
|
contactProvider.setCurrentUserId(authProvider.currentUserId);
|
||||||
await contactProvider.loadContacts();
|
await contactProvider.loadContacts(enrichContacts: false);
|
||||||
|
|
||||||
final contact = contactProvider.contacts.firstWhere(
|
final contact = contactProvider.contacts.firstWhere(
|
||||||
(c) => c.id == targetChatId,
|
(c) => c.id == targetChatId,
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class ContactTile extends StatelessWidget {
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
contact.lastMessage ?? "Нет сообщений",
|
contact.isLastMsgDecrypted ? contact.lastMessage ?? "Нет сообщений" : (contact.lastMessage != null ? "Ожидание дешифровки..." : "Нет сообщений"),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(color: Colors.grey),
|
style: const TextStyle(color: Colors.grey),
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
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:
|
cross_file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ dependencies:
|
||||||
dio: ^5.9.2
|
dio: ^5.9.2
|
||||||
package_info_plus: ^9.0.1
|
package_info_plus: ^9.0.1
|
||||||
open_filex: ^4.3.2
|
open_filex: ^4.3.2
|
||||||
|
convert: ^3.1.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from app.core.security import get_current_user
|
||||||
from app.api import schemas
|
from app.api import schemas
|
||||||
from sqlalchemy import or_, and_, exists
|
from sqlalchemy import or_, and_, exists
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from app.websocket import connection_manager
|
||||||
|
|
||||||
# бд
|
# бд
|
||||||
def get_db():
|
def get_db():
|
||||||
|
|
@ -146,7 +146,8 @@ async def update_privacy_settings(
|
||||||
user_to_update.show_about = 1 if data.show_about else 0
|
user_to_update.show_about = 1 if data.show_about else 0
|
||||||
if data.show_username is not None:
|
if data.show_username is not None:
|
||||||
user_to_update.show_username = 1 if data.show_username else 0
|
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:
|
try:
|
||||||
db.commit()
|
db.commit()
|
||||||
except Exception:
|
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_avatar": bool(current_user.show_avatar),
|
||||||
"show_about": bool(current_user.show_about),
|
"show_about": bool(current_user.show_about),
|
||||||
"show_username": bool(current_user.show_username),
|
"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:
|
if user.show_email:
|
||||||
profile_data["email"] = user.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
|
return profile_data
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ class User(Base):
|
||||||
show_avatar = Column(Integer, nullable=False, server_default="1")
|
show_avatar = Column(Integer, nullable=False, server_default="1")
|
||||||
show_about = 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_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):
|
class Message(Base):
|
||||||
__tablename__ = "messages"
|
__tablename__ = "messages"
|
||||||
|
|
@ -93,6 +95,11 @@ def _ensure_sqlite_user_columns():
|
||||||
conn.execute(text("ALTER TABLE users ADD COLUMN show_about INTEGER DEFAULT 1"))
|
conn.execute(text("ALTER TABLE users ADD COLUMN show_about INTEGER DEFAULT 1"))
|
||||||
if "show_username" not in existing:
|
if "show_username" not in existing:
|
||||||
conn.execute(text("ALTER TABLE users ADD COLUMN show_username INTEGER DEFAULT 1"))
|
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()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
from fastapi import HTTPException, status, APIRouter, WebSocket, WebSocketDisconnect, Query, Depends
|
from fastapi import HTTPException, status, APIRouter, WebSocket, WebSocketDisconnect, Query, Depends
|
||||||
from app.core.security import test_token
|
from app.core.security import test_token
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
import json
|
import json
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.db import models
|
from app.db import models
|
||||||
from firebase_admin import messaging, credentials, exceptions
|
from firebase_admin import messaging, credentials, exceptions
|
||||||
import firebase_admin
|
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)
|
firebase_admin.initialize_app(cred)
|
||||||
|
|
||||||
# бд
|
# бд
|
||||||
|
|
@ -40,6 +41,10 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db:
|
||||||
print("ПОДКЛЮЧЕНИЕ")
|
print("ПОДКЛЮЧЕНИЕ")
|
||||||
await manager.connect(user_id, websocket)
|
await manager.connect(user_id, websocket)
|
||||||
print("ПОДКЛЮЧЕНО")
|
print("ПОДКЛЮЧЕНО")
|
||||||
|
|
||||||
|
db.query(models.User).filter(models.User.id == user_id).update({"last_online": datetime.now(timezone.utc)},
|
||||||
|
synchronize_session="fetch")
|
||||||
|
db.commit()
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
print("ОЖИДАНИЕ СООБЩЕНИЙ")
|
print("ОЖИДАНИЕ СООБЩЕНИЙ")
|
||||||
|
|
@ -47,9 +52,14 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db:
|
||||||
message_data = json.loads(data)
|
message_data = json.loads(data)
|
||||||
print(f"DEBUG: Получены данные: {message_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":
|
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")
|
receiver_id = message_data.get("receiver_id")
|
||||||
temp_id = message_data.get("temp_id")
|
temp_id = message_data.get("temp_id")
|
||||||
content = message_data.get("content")
|
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",
|
"detail": "message_id must be int",
|
||||||
})
|
})
|
||||||
continue
|
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:
|
if msg is None or msg.sender_id != user_id:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
|
|
@ -186,7 +197,8 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db:
|
||||||
"detail": "message_id must be int",
|
"detail": "message_id must be int",
|
||||||
})
|
})
|
||||||
continue
|
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:
|
if msg is None or msg.sender_id != user_id:
|
||||||
continue
|
continue
|
||||||
receiver_id = msg.receiver_id
|
receiver_id = msg.receiver_id
|
||||||
|
|
@ -210,7 +222,8 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db:
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
continue
|
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:
|
if msg is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -237,10 +250,15 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
manager.disconnect(user_id)
|
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):
|
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(
|
message = messaging.Message(
|
||||||
data={
|
data={
|
||||||
"type": "enc_message",
|
"type": "enc_message",
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ app = FastAPI()
|
||||||
app.include_router(auth.authRouter)
|
app.include_router(auth.authRouter)
|
||||||
app.include_router(users.usersRouter)
|
app.include_router(users.usersRouter)
|
||||||
app.include_router(messages.messagesRouter)
|
app.include_router(messages.messagesRouter)
|
||||||
app.include_router(media.mediaRouter)
|
#app.include_router(media.mediaRouter)
|
||||||
app.include_router(wsRouter)
|
app.include_router(wsRouter)
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue