Добавлены Пуши
This commit is contained in:
parent
40d715539e
commit
1b8670d811
|
|
@ -14,6 +14,11 @@ migrate_working_dir/
|
|||
*.db
|
||||
.env
|
||||
venv/
|
||||
.venv/
|
||||
chepuhagram-6ca5d-firebase-adminsdk-fbsvc-cf8a5ad2f3.json
|
||||
.firebaserc
|
||||
firebase-tools-instant-win.exe
|
||||
google-services.json
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
|
|
|
|||
|
|
@ -3,14 +3,16 @@ plugins {
|
|||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
id("com.google.gms.google-services")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.chepuhagram"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
namespace = "ru.chepuhagram.app"
|
||||
compileSdk = 36
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
|
@ -21,11 +23,11 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.example.chepuhagram"
|
||||
applicationId = "ru.chepuhagram.app"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
targetSdk = 36
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
|
@ -42,3 +44,9 @@ android {
|
|||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
|
||||
implementation(platform("com.google.firebase:firebase-bom:34.12.0"))
|
||||
implementation("com.google.firebase:firebase-messaging")
|
||||
}
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:label="Chepuhagram"
|
||||
android:name="${applicationName}"
|
||||
|
|
@ -25,6 +28,16 @@
|
|||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Firebase Messaging Service -->
|
||||
<service
|
||||
android:name="com.google.firebase.messaging.FirebaseMessagingService"
|
||||
android:exported="false">
|
||||
<intent-filter android:priority="-500">
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.example.chepuhagram
|
||||
package ru.chepuhagram.app
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
|
|
@ -22,3 +22,7 @@ subprojects {
|
|||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.google.gms.google-services") version "4.4.4" apply false
|
||||
}
|
||||
|
|
@ -64,6 +64,32 @@ class ApiService extends ChangeNotifier {
|
|||
return token;
|
||||
}
|
||||
|
||||
Future<bool> updateFcmToken(String fcmtoken) async {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final token = await getAccessToken();
|
||||
final response = await _client.post(
|
||||
Uri.http(AppConstants.baseUrl, 'auth/update-fcm', {'token': fcmtoken}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $token',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return true;
|
||||
} else {
|
||||
print("Ошибка установки ключа: ${response.statusCode}");
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> setPublicKey(String publicKey) async {
|
||||
notifyListeners();
|
||||
|
||||
|
|
|
|||
233
lib/main.dart
233
lib/main.dart
|
|
@ -1,13 +1,174 @@
|
|||
import 'package:chepuhagram/presentation/screens/splash_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'dart:io';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'data/datasources/ws_client.dart';
|
||||
import 'logic/auth_provider.dart';
|
||||
import 'logic/contact_provider.dart';
|
||||
import 'core/theme_manager.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:chepuhagram/domain/services/crypto_service.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:chepuhagram/presentation/screens/chat_screen.dart';
|
||||
import 'package:chepuhagram/presentation/screens/contacts_screen.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'presentation/screens/splash_screen.dart';
|
||||
|
||||
void main() {
|
||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
// Глобальная переменная для отслеживания текущего активного контакта в чате
|
||||
int? currentActiveChatContactId;
|
||||
|
||||
// Глобальная переменная для хранения начального сообщения (при запуске из уведомления)
|
||||
RemoteMessage? initialMessage;
|
||||
|
||||
// Ключ для SharedPreferences
|
||||
const String _notificationLaunchKey = 'notification_launch_data';
|
||||
|
||||
Future<void> _onSelectNotification(NotificationResponse notificationResponse) async {
|
||||
final payload = notificationResponse.payload;
|
||||
if (payload != null) {
|
||||
try {
|
||||
final data = jsonDecode(payload);
|
||||
final senderId = int.tryParse(data['sender_id']?.toString() ?? '');
|
||||
if (senderId != null) {
|
||||
print('Notification selected, payload sender_id=$senderId');
|
||||
|
||||
final context = navigatorKey.currentContext;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Важно: не сохраняем payload в SharedPreferences, если можем сразу перейти в чат.
|
||||
// Иначе при следующем обычном запуске (по иконке) останется "хвост" и приложение
|
||||
// будет снова автопереходить в чат.
|
||||
if (context == null) {
|
||||
await prefs.setString(_notificationLaunchKey, jsonEncode(data));
|
||||
print('Navigator context is null, saved notification payload to SharedPreferences');
|
||||
} else {
|
||||
await prefs.remove(_notificationLaunchKey);
|
||||
}
|
||||
|
||||
// Navigate to chat with this contact (if context is ready)
|
||||
_navigateToChat(senderId);
|
||||
} else {
|
||||
print('Notification payload has invalid sender_id: ${data['sender_id']}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error parsing notification payload: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _navigateToChat(int senderId) {
|
||||
print('Navigating to chat with senderId: $senderId');
|
||||
final context = navigatorKey.currentContext;
|
||||
if (context != null) {
|
||||
final contactProvider = Provider.of<ContactProvider>(context, listen: false);
|
||||
|
||||
// Check if contacts are loaded
|
||||
if (contactProvider.contacts.isEmpty) {
|
||||
print('Contacts not loaded yet, navigating to contacts screen first');
|
||||
// Navigate to contacts screen and pass the senderId to navigate to chat later
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ContactsScreen(targetChatId: senderId),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final contact = contactProvider.contacts.firstWhere(
|
||||
(c) => c.id == senderId,
|
||||
orElse: () => throw Exception('Contact not found'),
|
||||
);
|
||||
print('Found contact: ${contact.username}, navigating to chat');
|
||||
currentActiveChatContactId = senderId; // Устанавливаем активный чат
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ChatScreen(contact: contact),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
print('Contact with id $senderId not found, navigating to contacts screen');
|
||||
// Contact not found, go to contacts screen
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const ContactsScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
print('Navigator context is null');
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
// Сохраняем информацию в SharedPreferences для надежности
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (initialMessage != null) {
|
||||
print('App launched from notification: ${initialMessage!.data}');
|
||||
print('Message type: ${initialMessage!.data['type']}');
|
||||
print('Sender ID: ${initialMessage!.data['sender_id']}');
|
||||
|
||||
// Сохраняем данные уведомления
|
||||
await prefs.setString(_notificationLaunchKey, jsonEncode(initialMessage!.data));
|
||||
print('Saved notification data to SharedPreferences');
|
||||
} else {
|
||||
print('No initial message - app launched normally');
|
||||
// Очищаем сохраненные данные, если приложение запущено нормально
|
||||
await prefs.remove(_notificationLaunchKey);
|
||||
}
|
||||
|
||||
// Initialize local notifications
|
||||
const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
final InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid);
|
||||
await flutterLocalNotificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
onDidReceiveNotificationResponse: _onSelectNotification,
|
||||
);
|
||||
|
||||
// Если приложение было запущено из локального уведомления, сохраним payload
|
||||
final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
|
||||
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
|
||||
final payload = notificationAppLaunchDetails?.notificationResponse?.payload;
|
||||
print('App launched from local notification, payload: $payload');
|
||||
if (payload != null && payload.isNotEmpty) {
|
||||
try {
|
||||
final data = jsonDecode(payload);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_notificationLaunchKey, jsonEncode(data));
|
||||
print('Saved local notification launch payload to SharedPreferences');
|
||||
} catch (e) {
|
||||
print('Failed to save notification launch payload: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create notification channel for Android 8+
|
||||
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||||
'chat_id', // id
|
||||
'Messages', // title
|
||||
description: 'Chat messages notifications', // description
|
||||
importance: Importance.high,
|
||||
);
|
||||
|
||||
await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel(channel);
|
||||
|
||||
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
|
|
@ -22,6 +183,69 @@ void main() {
|
|||
);
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
print("Фоновый пуш получен: ${message.data}");
|
||||
if (message.data['type'] == 'enc_message') {
|
||||
try {
|
||||
// Initialize notifications for background
|
||||
const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
const InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid);
|
||||
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
|
||||
|
||||
// Create notification channel
|
||||
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||||
'chat_id',
|
||||
'Messages',
|
||||
description: 'Chat messages notifications',
|
||||
importance: Importance.high,
|
||||
);
|
||||
|
||||
await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel(channel);
|
||||
|
||||
// Try to decrypt
|
||||
String notificationText = 'New encrypted message';
|
||||
try {
|
||||
// 1. Инициализируем крипто-сервис
|
||||
final crypto = CryptoService();
|
||||
|
||||
// 2. Достаем ключи (они должны быть в SecureStorage)
|
||||
final myPrivKey = await crypto.getPrivateKey();
|
||||
print('Private key retrieved: ${myPrivKey != null}');
|
||||
if (myPrivKey == null) {
|
||||
print('Private key not found, showing encrypted message');
|
||||
notificationText = 'Encrypted message: ${message.data['content']?.substring(0, 50) ?? 'N/A'}...';
|
||||
} else {
|
||||
// 3. Расшифровываем
|
||||
final sharedSecret = await crypto.deriveSharedSecret(myPrivKey, message.data['public_key']);
|
||||
final decryptedText = await crypto.decryptMessage(message.data['content'], sharedSecret);
|
||||
notificationText = decryptedText;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Decryption failed: $e');
|
||||
notificationText = 'Failed to decrypt: ${e.toString()}';
|
||||
}
|
||||
|
||||
// 4. Показываем локальное уведомление
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
message.hashCode,
|
||||
message.data['username'] ?? 'Unknown',
|
||||
notificationText,
|
||||
const NotificationDetails(android: AndroidNotificationDetails('chat_id', 'Messages')),
|
||||
payload: jsonEncode({
|
||||
'type': 'enc_message',
|
||||
'sender_id': message.data['sender_id'],
|
||||
}),
|
||||
);
|
||||
print('Notification shown successfully');
|
||||
} catch (e) {
|
||||
print('Error processing background message: $e');
|
||||
}
|
||||
} else {
|
||||
print('Message type is not enc_message: ${message.data['type']}');
|
||||
}
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
|
|
@ -35,6 +259,7 @@ class MyApp extends StatelessWidget {
|
|||
themeAnimationCurve: Curves.easeInOut,
|
||||
theme: themeProvider.themeData,
|
||||
themeMode: themeProvider.themeMode,
|
||||
navigatorKey: navigatorKey,
|
||||
|
||||
// Начальный экран
|
||||
home: const SplashScreen(),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import '/data/models/message_model.dart';
|
||||
import '/data/models/contact_model.dart';
|
||||
|
|
@ -5,11 +7,13 @@ import 'package:chepuhagram/presentation/widgets/message_bubble.dart';
|
|||
import 'package:chepuhagram/data/repositories/contact_repository.dart';
|
||||
import 'package:chepuhagram/domain/services/crypto_service.dart';
|
||||
import 'package:chepuhagram/data/datasources/ws_client.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:provider/provider.dart';
|
||||
import '/logic/contact_provider.dart';
|
||||
import '../../domain/services/api_service.dart';
|
||||
import 'package:chepuhagram/data/datasources/local_db_service.dart';
|
||||
import 'package:chepuhagram/main.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'contacts_screen.dart';
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
final Contact contact;
|
||||
|
|
@ -21,6 +25,7 @@ class ChatScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ChatScreenState extends State<ChatScreen> {
|
||||
static const String _notificationLaunchKey = 'notification_launch_data';
|
||||
int myId = 0;
|
||||
late Contact _currentContact;
|
||||
bool _isKeyLoading = false;
|
||||
|
|
@ -29,32 +34,41 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
final apiService = ApiService();
|
||||
final CryptoService _cryptoService = CryptoService();
|
||||
List<MessageModel> messages = [];
|
||||
StreamSubscription<dynamic>? _socketSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentContact = widget.contact;
|
||||
|
||||
currentActiveChatContactId = _currentContact.id; // Устанавливаем активный чат
|
||||
final contactProvider = context.read<ContactProvider>();
|
||||
myId = contactProvider.getCurrentUserId() ?? 0;
|
||||
_loadHistory();
|
||||
// Если ключа нет, загружаем его при входе
|
||||
if (_currentContact.publicKey == null) {
|
||||
_loadContactKey();
|
||||
}
|
||||
_loadHistory();
|
||||
|
||||
final socketService = Provider.of<SocketService>(context, listen: false);
|
||||
_socketSubscription = socketService.messages.listen(_handleIncomingMessage);
|
||||
}
|
||||
|
||||
Future<void> _loadContactKey() async {
|
||||
if (!mounted) return;
|
||||
setState(() => _isKeyLoading = true);
|
||||
try {
|
||||
final updatedContact = await _contactRepository.fetchContactById(
|
||||
_currentContact.id,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_currentContact = updatedContact;
|
||||
_isKeyLoading = false;
|
||||
});
|
||||
print(updatedContact.publicKey);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _isKeyLoading = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
|
|
@ -66,6 +80,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
currentActiveChatContactId = null; // Сбрасываем активный чат
|
||||
_socketSubscription?.cancel();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
|
@ -73,7 +89,21 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(_currentContact.name)),
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
title: Text(_currentContact.name),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
|
|
@ -146,11 +176,17 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
sharedSecret,
|
||||
);
|
||||
|
||||
final encryptedText50 = await _cryptoService.encryptMessage(
|
||||
rawText.length > 50 ? rawText.substring(0, 50) : rawText,
|
||||
sharedSecret,
|
||||
);
|
||||
|
||||
// Формируем payload для сервера
|
||||
final payload = {
|
||||
"type": "private_message",
|
||||
"receiver_id": _currentContact.id,
|
||||
"content": encryptedText,
|
||||
"content50": encryptedText50,
|
||||
};
|
||||
|
||||
// Отправляем
|
||||
|
|
@ -180,16 +216,6 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
// Подписываемся на поток сообщений из сокета
|
||||
final socketService = Provider.of<SocketService>(context, listen: false);
|
||||
|
||||
socketService.messages.listen((rawData) {
|
||||
_handleIncomingMessage(rawData);
|
||||
});
|
||||
}
|
||||
|
||||
void _handleIncomingMessage(Map<String, dynamic> data) async {
|
||||
if (data['type'] == 'private_message') {
|
||||
|
|
@ -214,6 +240,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
|
||||
// 4. Добавляем в список и обновляем экран
|
||||
await LocalDbService().saveMessages([data]);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
messages.add(
|
||||
MessageModel(
|
||||
|
|
@ -238,6 +265,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
}
|
||||
|
||||
Future<void> _loadHistory() async {
|
||||
|
||||
initialMessage = null; // Сбрасываем данные уведомления при загрузке ключа
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_notificationLaunchKey);
|
||||
await prefs.setString(_notificationLaunchKey, ''); // Очищаем данные уведомления при загрузке ключа
|
||||
try {
|
||||
final myPrivKey = await _cryptoService.getPrivateKey();
|
||||
final sharedSecret = await _cryptoService.deriveSharedSecret(
|
||||
|
|
@ -265,6 +297,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
);
|
||||
}
|
||||
if (cached.isNotEmpty) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
messages = loadedLocalMessages;
|
||||
_isKeyLoading = false;
|
||||
|
|
@ -275,14 +308,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
}
|
||||
|
||||
final history = await apiService.getChatHistory(widget.contact.id);
|
||||
|
||||
print(history);
|
||||
List<MessageModel> loadedMessages = [];
|
||||
for (var msg in history) {
|
||||
final decrypted = await _cryptoService.decryptMessage(
|
||||
msg['content'],
|
||||
sharedSecret,
|
||||
);
|
||||
loadedMessages.add(
|
||||
loadedMessages.insert(
|
||||
0,
|
||||
MessageModel(
|
||||
text: decrypted,
|
||||
isMe: msg['sender_id'] == myId,
|
||||
|
|
@ -292,14 +326,20 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
),
|
||||
);
|
||||
}
|
||||
await localDb.saveMessages(history);
|
||||
try {
|
||||
await localDb.saveMessages(history);
|
||||
} catch (e) {
|
||||
print("Ошибка сохранения истории в локальную базу: $e");
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
messages = loadedMessages;
|
||||
_isKeyLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
print("Ошибка загрузки истории: $e");
|
||||
if (!mounted) return;
|
||||
setState(() => _isKeyLoading = false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:convert';
|
||||
import 'package:chepuhagram/domain/services/aPI_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../widgets/contact_tile.dart';
|
||||
|
|
@ -6,28 +8,249 @@ import '../screens/new_chat_screen.dart';
|
|||
import '../screens/chat_screen.dart';
|
||||
import '/logic/contact_provider.dart';
|
||||
import '/logic/auth_provider.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:chepuhagram/domain/services/crypto_service.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:chepuhagram/main.dart';
|
||||
|
||||
class ContactsScreen extends StatefulWidget {
|
||||
const ContactsScreen({super.key});
|
||||
final int? targetChatId;
|
||||
|
||||
const ContactsScreen({super.key, this.targetChatId});
|
||||
|
||||
@override
|
||||
State<ContactsScreen> createState() => _ContactsScreenState();
|
||||
}
|
||||
|
||||
class _ContactsScreenState extends State<ContactsScreen> {
|
||||
static const String _notificationLaunchKey = 'notification_launch_data';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
print(
|
||||
'ContactsScreen initState, targetChatId: ${widget.targetChatId}',
|
||||
);
|
||||
_setupPushNotifications();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final authProvider = context.read<AuthProvider>();
|
||||
final contactProvider = context.read<ContactProvider>();
|
||||
|
||||
// Установить текущего пользователя и загрузить контакты с сообщениями
|
||||
contactProvider.setCurrentUserId(authProvider.currentUserId);
|
||||
contactProvider.loadContacts();
|
||||
contactProvider.loadContacts().then((_) {
|
||||
print(
|
||||
'Contacts loaded, checking targetChatId: ${widget.targetChatId}',
|
||||
);
|
||||
// После загрузки контактов проверить, нужно ли перейти к чату
|
||||
if (widget.targetChatId != null) {
|
||||
_navigateToTargetChat();
|
||||
} else {
|
||||
_checkSavedNotificationTarget();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _checkSavedNotificationTarget() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final savedData = prefs.getString(_notificationLaunchKey);
|
||||
if (savedData == null) {
|
||||
print('No saved notification data found in SharedPreferences');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final data = jsonDecode(savedData) as Map<String, dynamic>;
|
||||
print('Recovered saved notification data: $data');
|
||||
final senderId = int.tryParse(data['sender_id']?.toString() ?? '');
|
||||
final type = data['type']?.toString();
|
||||
|
||||
// Поддерживаем старый payload (только sender_id) и новый (type+sender_id)
|
||||
if (senderId != null && (type == null || type == 'enc_message')) {
|
||||
print('Recovered targetChatId from saved data: $senderId');
|
||||
|
||||
await prefs.remove(_notificationLaunchKey);
|
||||
_navigateToTargetChatWithId(senderId);
|
||||
return;
|
||||
}
|
||||
|
||||
print('Saved notification data is not a valid payload: $data');
|
||||
await prefs.remove(_notificationLaunchKey);
|
||||
} catch (e) {
|
||||
print('Error parsing saved notification data: $e');
|
||||
await prefs.remove(_notificationLaunchKey);
|
||||
}
|
||||
}
|
||||
|
||||
void _navigateToTargetChat() {
|
||||
if (widget.targetChatId == null) return;
|
||||
|
||||
_navigateToTargetChatWithId(widget.targetChatId!);
|
||||
}
|
||||
|
||||
void _navigateToTargetChatWithId(int targetChatId) {
|
||||
print(
|
||||
'_navigateToTargetChat called with targetChatId: $targetChatId',
|
||||
);
|
||||
final contactProvider = context.read<ContactProvider>();
|
||||
try {
|
||||
final contact = contactProvider.contacts.firstWhere(
|
||||
(c) => c.id == targetChatId,
|
||||
);
|
||||
print(
|
||||
'Auto-navigating to chat with contact: ${contact.username}',
|
||||
);
|
||||
currentActiveChatContactId = targetChatId; // Устанавливаем активный чат
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => ChatScreen(contact: contact)),
|
||||
);
|
||||
} catch (e) {
|
||||
print('Target contact with id $targetChatId not found: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setupPushNotifications() async {
|
||||
// Request permissions
|
||||
await FirebaseMessaging.instance.requestPermission();
|
||||
|
||||
String? token = await FirebaseMessaging.instance.getToken();
|
||||
if (token != null) {
|
||||
ApiService apiService = ApiService();
|
||||
print(token);
|
||||
await apiService.updateFcmToken(token);
|
||||
}
|
||||
|
||||
// Listen for token refresh
|
||||
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
|
||||
ApiService apiService = ApiService();
|
||||
apiService.updateFcmToken(newToken);
|
||||
print('FCM Token refreshed: $newToken');
|
||||
});
|
||||
|
||||
// Listen for foreground messages
|
||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
|
||||
print('Foreground message received: ${message.data}');
|
||||
if (message.data['type'] == 'enc_message') {
|
||||
_handleIncomingMessage(message);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle notification tap when app was terminated/backgrounded
|
||||
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
|
||||
print('Notification tapped, app opened: ${message.data}');
|
||||
if (message.data['type'] == 'enc_message') {
|
||||
final senderId = int.tryParse(
|
||||
message.data['sender_id']?.toString() ?? '',
|
||||
);
|
||||
if (senderId != null) {
|
||||
_navigateToChatFromNotification(senderId);
|
||||
} else {
|
||||
print(
|
||||
'Notification tap contains invalid sender_id: ${message.data['sender_id']}',
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _navigateToChatFromNotification(int senderId) {
|
||||
final contactProvider = context.read<ContactProvider>();
|
||||
print(
|
||||
'Navigate to chat from notification with senderId: $senderId',
|
||||
);
|
||||
|
||||
// Если контакты еще не загружены, ждем их загрузки
|
||||
if (contactProvider.contacts.isEmpty) {
|
||||
print('Contacts not loaded yet, waiting...');
|
||||
// Ждем немного и пробуем снова
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (mounted) {
|
||||
_navigateToChatFromNotification(senderId);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final contact = contactProvider.contacts.firstWhere(
|
||||
(c) => c.id == senderId,
|
||||
);
|
||||
print(
|
||||
'Navigating to chat from notification: ${contact.username}',
|
||||
);
|
||||
currentActiveChatContactId = senderId; // Устанавливаем активный чат
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => ChatScreen(contact: contact)),
|
||||
);
|
||||
} catch (e) {
|
||||
// Contact not found, stay on contacts screen
|
||||
print('Contact not found for notification: $senderId');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleIncomingMessage(RemoteMessage message) async {
|
||||
try {
|
||||
// Проверяем, не находимся ли мы уже в чате с отправителем
|
||||
final senderId = int.tryParse(
|
||||
message.data['sender_id']?.toString() ?? '',
|
||||
);
|
||||
if (senderId != null && currentActiveChatContactId == senderId) {
|
||||
print('Already in chat with sender $senderId, skipping notification');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure notification channel exists
|
||||
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||||
'chat_id',
|
||||
'Messages',
|
||||
description: 'Chat messages notifications',
|
||||
importance: Importance.high,
|
||||
);
|
||||
|
||||
await flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin
|
||||
>()
|
||||
?.createNotificationChannel(channel);
|
||||
|
||||
final crypto = CryptoService();
|
||||
final myPrivKey = await crypto.getPrivateKey();
|
||||
if (myPrivKey == null) {
|
||||
print('Private key not found, cannot decrypt message');
|
||||
return;
|
||||
}
|
||||
|
||||
final sharedSecret = await crypto.deriveSharedSecret(
|
||||
myPrivKey,
|
||||
message.data['public_key'],
|
||||
);
|
||||
final decryptedText = await crypto.decryptMessage(
|
||||
message.data['content'],
|
||||
sharedSecret,
|
||||
);
|
||||
|
||||
// Show local notification
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
message.hashCode,
|
||||
message.data['username'] ?? 'Unknown',
|
||||
decryptedText,
|
||||
const NotificationDetails(
|
||||
android: AndroidNotificationDetails('chat_id', 'Messages'),
|
||||
),
|
||||
payload: jsonEncode({
|
||||
'type': 'enc_message',
|
||||
'sender_id': message.data['sender_id'],
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error processing foreground message: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
|
@ -91,7 +314,11 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
|||
accountEmail: Text("@ArturKarasevich"),
|
||||
currentAccountPicture: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
child: Icon(Icons.person, size: 40, color: Theme.of(context).colorScheme.primaryContainer,),
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
size: 40,
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:chepuhagram/presentation/screens/contacts_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../logic/auth_provider.dart';
|
||||
|
|
@ -98,7 +99,10 @@ class _KeyRecoveryScreenState extends State<KeyRecoveryScreen> {
|
|||
|
||||
if (mounted) {
|
||||
// Возвращаемся на главный экран
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../logic/auth_provider.dart';
|
||||
import '../../logic/contact_provider.dart';
|
||||
import 'login_screen.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import 'account_setup_screen.dart';
|
||||
import 'key_recovery_screen.dart';
|
||||
import 'chat_screen.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:chepuhagram/main.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
const SplashScreen({super.key});
|
||||
|
|
@ -14,12 +20,36 @@ class SplashScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _SplashScreenState extends State<SplashScreen> {
|
||||
int? _targetChatId;
|
||||
|
||||
// Ключ для SharedPreferences
|
||||
static const String _notificationLaunchKey = 'notification_launch_data';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
print('SplashScreen initState');
|
||||
_setupNotificationHandler();
|
||||
_initializeApp();
|
||||
}
|
||||
|
||||
void _setupNotificationHandler() {
|
||||
print('Setting up notification handler');
|
||||
// Обработка открытия приложения из уведомления
|
||||
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
|
||||
print('App opened from notification: ${message.data}');
|
||||
if (message.data['type'] == 'enc_message') {
|
||||
final senderId = int.tryParse(message.data['sender_id']?.toString() ?? '');
|
||||
if (senderId != null) {
|
||||
setState(() {
|
||||
_targetChatId = senderId;
|
||||
});
|
||||
print('Set target chat from opened app: $senderId');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initializeApp() async {
|
||||
// 1. Искусственная задержка в 2 секунды для демонстрации splash
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
|
@ -51,9 +81,78 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||
);
|
||||
} else {
|
||||
// Путь Б: Нормальный вход в контакты
|
||||
// Проверяем, было ли приложение запущено из уведомления
|
||||
int? targetChatId = _targetChatId; // Сначала проверяем из onMessageOpenedApp
|
||||
|
||||
// Если не установлено, проверяем SharedPreferences
|
||||
if (targetChatId == null) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final savedData = prefs.getString(_notificationLaunchKey);
|
||||
|
||||
if (savedData != null) {
|
||||
try {
|
||||
final data = jsonDecode(savedData) as Map<String, dynamic>;
|
||||
print('Found saved notification data: $data');
|
||||
final senderId = int.tryParse(data['sender_id']?.toString() ?? '');
|
||||
final type = data['type']?.toString();
|
||||
|
||||
// Поддерживаем старый payload (только sender_id) и новый (type+sender_id)
|
||||
if (senderId != null && (type == null || type == 'enc_message')) {
|
||||
targetChatId = senderId;
|
||||
print('App launched from saved notification, target chat: $targetChatId');
|
||||
}
|
||||
|
||||
// Очищаем сохраненные данные после использования
|
||||
await prefs.remove(_notificationLaunchKey);
|
||||
} catch (e) {
|
||||
print('Error parsing saved notification data: $e');
|
||||
await prefs.remove(_notificationLaunchKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Также проверяем initialMessage как fallback
|
||||
if (targetChatId == null) {
|
||||
print('Checking initialMessage: $initialMessage');
|
||||
if (initialMessage != null) {
|
||||
print('Initial message data: ${initialMessage!.data}');
|
||||
if (initialMessage!.data['type'] == 'enc_message') {
|
||||
targetChatId = int.tryParse(initialMessage!.data['sender_id']?.toString() ?? '');
|
||||
print('Set target chat from initialMessage: $targetChatId');
|
||||
} else {
|
||||
print('Initial message type is not enc_message: ${initialMessage!.data['type']}');
|
||||
}
|
||||
} else {
|
||||
print('No initial message found');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print('Using targetChatId from onMessageOpenedApp: $targetChatId');
|
||||
}
|
||||
|
||||
if (targetChatId != null) {
|
||||
print('Notification targetChatId resolved: $targetChatId, trying to open chat directly');
|
||||
try {
|
||||
final contactProvider = context.read<ContactProvider>();
|
||||
contactProvider.setCurrentUserId(authProvider.currentUserId);
|
||||
await contactProvider.loadContacts();
|
||||
|
||||
final contact = contactProvider.contacts.firstWhere((c) => c.id == targetChatId);
|
||||
currentActiveChatContactId = targetChatId;
|
||||
print('Directly navigating to ChatScreen for contact: ${contact.username}');
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => ChatScreen(contact: contact)),
|
||||
);
|
||||
return;
|
||||
} catch (e) {
|
||||
print('Failed to open chat directly, falling back to ContactsScreen: $e');
|
||||
}
|
||||
}
|
||||
|
||||
print('Navigating to ContactsScreen, targetChatId: $targetChatId');
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const ContactsScreen()),
|
||||
MaterialPageRoute(builder: (_) => ContactsScreen(targetChatId: targetChatId)),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -102,7 +201,7 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 80),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5,12 +5,22 @@
|
|||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import firebase_analytics
|
||||
import firebase_core
|
||||
import firebase_messaging
|
||||
import flutter_local_notifications
|
||||
import flutter_secure_storage_darwin
|
||||
import path_provider_foundation
|
||||
import shared_preferences_foundation
|
||||
import sqflite_darwin
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
|
||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
}
|
||||
|
|
|
|||
208
pubspec.lock
208
pubspec.lock
|
|
@ -1,6 +1,14 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_flutterfire_internals:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.35"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -73,6 +81,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.9"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.12"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -89,6 +105,86 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
firebase_analytics:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_analytics
|
||||
sha256: dbf1e7ab22cfb1f4a4adb103b46a26276b4edc593d4a78ef6fb942bafc92e035
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.10.7"
|
||||
firebase_analytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_analytics_platform_interface
|
||||
sha256: "3729b74f8cf1d974a27ba70332ecb55ff5ff560edc8164a6469f4a055b429c37"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.10.8"
|
||||
firebase_analytics_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_analytics_web
|
||||
sha256: "019cd7eee74254d33fbd2e29229367ce33063516bf6b3258a341d89e3b0f1655"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.7+7"
|
||||
firebase_core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.32.0"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
sha256: "8bcfad6d7033f5ea951d15b867622a824b13812178bfec0c779b9d81de011bbb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.2"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: eb3afccfc452b2b2075acbe0c4b27de62dd596802b4e5e19869c1e926cbb20b3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.24.0"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: "980259425fa5e2afc03e533f33723335731d21a56fd255611083bceebf4373a8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.7.10"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: "87c4a922cb6f811cfb7a889bdbb3622702443c52a0271636cbc90d813ceac147"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.37"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: "90dc7ed885e90a24bb0e56d661d4d2b5f84429697fd2cbb9e5890a0ca370e6f4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.5.18"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
|
@ -102,6 +198,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "17.2.4"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_linux
|
||||
sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
flutter_local_notifications_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_platform_interface
|
||||
sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.0"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -192,6 +312,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.7"
|
||||
jwt_decoder:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -328,6 +456,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -352,6 +488,62 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5+1"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.5"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.23"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.6"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
|
@ -453,6 +645,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.7"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timezone
|
||||
sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -517,6 +717,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
sdks:
|
||||
dart: ">=3.10.0 <4.0.0"
|
||||
flutter: ">=3.38.0"
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@ dependencies:
|
|||
cryptography: ^2.5.0
|
||||
sqflite: ^2.3.0
|
||||
path: ^1.9.0
|
||||
firebase_core: ^2.24.2
|
||||
firebase_messaging: ^14.7.10
|
||||
flutter_local_notifications: ^17.2.2
|
||||
firebase_analytics: ^10.10.7
|
||||
shared_preferences: ^2.5.5
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: b577fae9f973
|
||||
Revises: 4e1aa78f81c6
|
||||
Create Date: 2026-04-24 23:01:14.120776
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'b577fae9f973'
|
||||
down_revision: Union[str, Sequence[str], None] = '4e1aa78f81c6'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('fcm_token', sa.String(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||
batch_op.drop_column('fcm_token')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -6,7 +6,6 @@ from app.api import schemas
|
|||
from app.db import models
|
||||
from jose import JWTError, jwt
|
||||
from app.core.security import get_current_user
|
||||
|
||||
# бд
|
||||
|
||||
|
||||
|
|
@ -106,3 +105,11 @@ async def setup_account(data: schemas.SetupAccount, current_user: models.User =
|
|||
db.commit()
|
||||
db.refresh(user_to_update)
|
||||
return {"status": "ok", "message": "Account setup completed"}
|
||||
|
||||
@authRouter.post("/update-fcm")
|
||||
async def update_fcm(token: str, current_user: models.User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
user_to_update = db.merge(current_user)
|
||||
user_to_update.fcm_token = token
|
||||
db.commit()
|
||||
db.refresh(user_to_update)
|
||||
return {"status": "ok"}
|
||||
|
|
@ -29,7 +29,7 @@ async def get_chat_history(
|
|||
messages = db.query(models.Message).filter(
|
||||
(models.Message.sender_id == current_user.id) & (models.Message.receiver_id == contact_id) |
|
||||
(models.Message.sender_id == contact_id) & (models.Message.receiver_id == current_user.id)
|
||||
).order_by(models.Message.timestamp.asc()).limit(limit).all()
|
||||
).order_by(models.Message.timestamp.desc()).limit(limit).all()
|
||||
|
||||
return messages
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class User(Base):
|
|||
hashed_password = Column(String)
|
||||
public_key = Column(String, nullable=True)
|
||||
encrypted_private_key = Column(String, nullable=True)
|
||||
fcm_token = Column(String, nullable=True)
|
||||
|
||||
class Message(Base):
|
||||
__tablename__ = "messages"
|
||||
|
|
|
|||
|
|
@ -5,9 +5,15 @@ from datetime import datetime
|
|||
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")
|
||||
firebase_admin.initialize_app(cred)
|
||||
|
||||
# бд
|
||||
|
||||
|
||||
def get_db():
|
||||
db = models.SessionLocal()
|
||||
try:
|
||||
|
|
@ -42,8 +48,11 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db:
|
|||
print(f"DEBUG: Получены данные: {message_data}")
|
||||
|
||||
if message_data.get("type") == "private_message":
|
||||
|
||||
user = db.query(models.User).filter(models.User.id == user_id).first()
|
||||
receiver_id = message_data.get("receiver_id")
|
||||
content = message_data.get("content")
|
||||
content50 = message_data.get("content50")
|
||||
new_msg = models.Message(
|
||||
sender_id=user_id,
|
||||
receiver_id=receiver_id,
|
||||
|
|
@ -52,6 +61,19 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db:
|
|||
db.add(new_msg)
|
||||
db.commit()
|
||||
db.refresh(new_msg)
|
||||
|
||||
if receiver_id not in manager.active_connections and user.public_key != '':
|
||||
receiver = db.query(models.User).filter(
|
||||
models.User.id == receiver_id).first()
|
||||
if receiver.fcm_token:
|
||||
send_fcm_notification(
|
||||
receiver.fcm_token,
|
||||
user_id,
|
||||
user.first_name,
|
||||
user.public_key,
|
||||
content50 if content50 else content,
|
||||
datetime.now(),
|
||||
)
|
||||
# Формируем пакет для получателя
|
||||
outgoing_message = {
|
||||
"id": new_msg.id,
|
||||
|
|
@ -70,6 +92,29 @@ async def websocket_endpoint(websocket: WebSocket, token: str = Query(None), db:
|
|||
manager.disconnect(user_id)
|
||||
|
||||
|
||||
def send_fcm_notification(token, user_id, username, public_key, encrypted_text, timestamp):
|
||||
print(f"DEBUG: Отправляем FCM уведомление пользователю {user_id} с токеном {token}")
|
||||
message = messaging.Message(
|
||||
data={
|
||||
"type": "enc_message",
|
||||
"sender_id": str(user_id),
|
||||
"username": username,
|
||||
"public_key": public_key,
|
||||
"content": encrypted_text, # Зашифрованный текст
|
||||
"timestamp": timestamp.isoformat(),
|
||||
},
|
||||
android=messaging.AndroidConfig(
|
||||
priority='high',
|
||||
),
|
||||
token=token,
|
||||
)
|
||||
try:
|
||||
response = messaging.send(message)
|
||||
print('Successfully sent message:', response)
|
||||
except Exception as e:
|
||||
print('Unexpected error sending push:', e)
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
# Храним активные соединения: {user_id: websocket}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,12 @@
|
|||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FirebaseCorePluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
firebase_core
|
||||
flutter_secure_storage_windows
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue