import 'package:chepuhagram/data/datasources/ws_client.dart'; import 'package:chepuhagram/logic/auth_provider.dart'; import 'package:chepuhagram/logic/contact_provider.dart'; import 'package:chepuhagram/core/theme_manager.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'dart:io'; import 'package:firebase_core/firebase_core.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:path_provider/path_provider.dart'; import 'package:chepuhagram/domain/services/crypto_service.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'dart:convert'; import 'package:chepuhagram/data/datasources/local_db_service.dart'; 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 'package:path/path.dart' as p; import 'package:chepuhagram/presentation/screens/splash_screen.dart'; import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; import 'package:chepuhagram/presentation/screens/call_screen.dart'; import 'package:flutter_callkit_incoming/entities/entities.dart'; import 'package:timeago/timeago.dart' as timeago; import 'package:timeago/src/messages/ru_messages.dart'; final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); final GlobalKey navigatorKey = GlobalKey(); final RouteObserver routeObserver = RouteObserver(); int? currentActiveChatContactId; RemoteMessage? initialMessage; // Ключ для SharedPreferences const String _notificationLaunchKey = 'notification_launch_data'; const String _lastHandledNotificationLaunchPayloadKey = 'notification_last_handled_payload'; Future _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(); final canonicalPayload = jsonEncode(data); if (context == null) { final lastHandled = prefs.getString( _lastHandledNotificationLaunchPayloadKey, ); if (lastHandled != canonicalPayload) { await prefs.setString(_notificationLaunchKey, canonicalPayload); await prefs.setString( _lastHandledNotificationLaunchPayloadKey, canonicalPayload, ); } print( 'Navigator context is null, saved notification payload to SharedPreferences', ); } else { await prefs.remove(_notificationLaunchKey); } _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( context, listen: false, ); // ВАЖНОЕ ИЗМЕНЕНИЕ: // Независимо от того, загружены контакты или нет, на десктопе/планшете мы ВСЕГДА // сбрасываем навигацию до экрана ContactsScreen, передавая ему targetChatId. // Это заставит приложение открыть чат в его родном правом окошке, сохранив боковую панель. currentActiveChatContactId = senderId; // Устанавливаем активный чат // Очищаем весь стек экранов и открываем ContactsScreen со встроенным параметром чата Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (_) => ContactsScreen(targetChatId: senderId)), (route) => false, // Удаляет все предыдущие полноэкранные оверлеи ); } else { print('Navigator context is null'); } } bool firebaseInitialized = false; void main() async { // Initialize timeago for Russian locale timeago.setLocaleMessages('ru', timeago.RuMessages()); WidgetsFlutterBinding.ensureInitialized(); if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { sqfliteFfiInit(); // ИСПОЛЬЗУЕМ СУППОРТ-ДИРЕКТОРИЮ (App Data / Roaming) вместо Документов final appSupportDir = await getApplicationSupportDirectory(); // Создаем подпапку внутри AppData, там Windows никогда не выдаст ошибку 2 final sqfliteDbPath = p.join(appSupportDir.path, 'sqflite_databases'); await Directory(sqfliteDbPath).create(recursive: true); // Привязываем фабрику sqflite к новому безопасному пути databaseFactory = databaseFactoryFfi; await databaseFactory.setDatabasesPath(sqfliteDbPath); print('Безопасный путь для SQFlite на Windows: $sqfliteDbPath'); } try { print('Initializing LocalDbService in main...'); final db = LocalDbService(); print('База данных сохранена по пути: ${db.connection.connectionData}'); print('LocalDbService initialized successfully.'); } catch (e, st) { print('LocalDbService init failed in main: $e'); print(st); } if (Platform.isAndroid || Platform.isIOS) { await Firebase.initializeApp(); firebaseInitialized = true; initialMessage = await FirebaseMessaging.instance.getInitialMessage(); print('Initial message from main(): $initialMessage'); } else { print('Skipping Firebase initialization on desktop.'); } // Сохраняем информацию в 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']}'); final payloadString = jsonEncode(initialMessage!.data); final lastHandled = prefs.getString( _lastHandledNotificationLaunchPayloadKey, ); if (lastHandled != payloadString) { // Сохраняем данные уведомления await prefs.setString(_notificationLaunchKey, payloadString); await prefs.setString( _lastHandledNotificationLaunchPayloadKey, payloadString, ); print('Saved notification data to SharedPreferences'); } else { print('InitialMessage payload already handled earlier, skipping'); } } else { print('No initial message - app launched normally'); // Очищаем сохраненные данные, если приложение запущено нормально await prefs.remove(_notificationLaunchKey); } const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); const WindowsInitializationSettings initializationSettingsWindows = WindowsInitializationSettings( appName: 'Chepuhagram', appUserModelId: 'ru.ArturKarasevich.Chepuhagram', guid: '6c0af055-e0b5-4f10-9aed-c12dc078f949', ); final InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, windows: initializationSettingsWindows, ); await flutterLocalNotificationsPlugin.initialize( settings: 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 lastHandled = prefs.getString( _lastHandledNotificationLaunchPayloadKey, ); if (lastHandled != payload) { final data = jsonDecode(payload); await prefs.setString(_notificationLaunchKey, jsonEncode(data)); await prefs.setString( _lastHandledNotificationLaunchPayloadKey, payload, ); print('Saved local notification launch payload to SharedPreferences'); } else { print('Local notification payload already handled earlier, skipping'); } } 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); if (firebaseInitialized) { FirebaseMessaging.onBackgroundMessage( _firebaseMessagingBackgroundHandler, ); } } if (Platform.isAndroid || Platform.isIOS) { initCallkitListener(); } else { print('Skipping CallKit listener on desktop platform.'); } runApp( MultiProvider( providers: [ Provider(create: (_) => CryptoService()), Provider(create: (_) => SocketService()), ChangeNotifierProvider(create: (_) => AuthProvider()), ChangeNotifierProvider(create: (_) => ThemeProvider()), Provider(create: (_) => SocketService()), ChangeNotifierProvider( create: (context) => ContactProvider(context.read()), ), ], child: const MyApp(), ), ); } void initCallkitListener() { if (!(Platform.isAndroid || Platform.isIOS)) { print('Skipping CallKit event listener on non-mobile platform.'); return; } try { FlutterCallkitIncoming.onEvent.listen((event) { if (event == null) return; switch (event.event) { case Event.actionCallIncoming: // Звонок получен, но CallKit уже показал экран. // Здесь можно логировать или обновить статус в БД. print("Incoming call: ${event.body['id']}"); break; case Event.actionCallStart: // Исходящий звонок начат break; case Event.actionCallAccept: // ПОЛЬЗОВАТЕЛЬ НАЖАЛ "ПРИНЯТЬ" // 1. Уведомляем сервер (чтобы другая сторона узнала о начале звонка) SocketService().sendMessage({ "type": "call_accept", "call_id": event.body['id'], }); // 2. Переходим на экран звонка navigatorKey.currentState?.push( MaterialPageRoute( builder: (_) => CallScreen( callId: event.body['id'], isIncoming: true, callerName: event.body['nameCaller'] ?? 'Unknown', onAccept: () {}, onHangup: () => _handleHangupGlobal(event.body['id']), ), ), ); break; case Event.actionCallDecline: // ПОЛЬЗОВАТЕЛЬ НАЖАЛ "ОТКЛОНИТЬ" SocketService().sendMessage({ "type": "decline", "call_id": event.body['id'], }); break; case Event.actionCallEnded: case Event.actionCallTimeout: // Звонок завершен или пропущен print("Call ended or timeout"); break; default: print("Event unhandled: ${event.event}"); break; } }); } catch (e, st) { print('CallKit listener initialization failed: $e'); print(st); } } void _handleHangupGlobal(String callId) { SocketService().sendMessage({"type": "hangup", "call_id": callId}); } @pragma('vm:entry-point') Future _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 WindowsInitializationSettings initializationSettingsWindows = WindowsInitializationSettings( appName: 'Chepuhagram', appUserModelId: 'ru.ArturKarasevich.Chepuhagram', guid: '6c0af055-e0b5-4f10-9aed-c12dc078f949', ); final InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, windows: initializationSettingsWindows, ); await flutterLocalNotificationsPlugin.initialize( settings: initializationSettings, onDidReceiveNotificationResponse: _onSelectNotification, ); // 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()}'; } final String senderIdRaw = message.data['sender_id']?.toString() ?? ''; final senderId = int.tryParse(senderIdRaw); // ========================================================== // ОБРАБОТКА ИМЕНИ: КЭШ УСТРОЙСТВА + ДАННЫЕ ОТ СЕРВЕРА // ========================================================== final prefs = await SharedPreferences.getInstance(); // 1. Пытаемся прочитать из локального кэша устройства final String? cachedFirstName = prefs.getString('firstname_$senderIdRaw'); final String? cachedLastName = prefs.getString('lastname_$senderIdRaw'); // Список «мусорных» системных строк для фильтрации final invalidValues = {'unknown', 'uncnown', 'null', ''}; // Функция очистки строк от пробелов и мусора String cleanField(String? value) { if (value == null) return ''; final trimmed = value.trim(); return invalidValues.contains(trimmed.toLowerCase()) ? '' : trimmed; } // Очищаем данные из кэша String firstName = cleanField(cachedFirstName); String lastName = cleanField(cachedLastName); // 2. Если в кэше пусто, берем данные, которые сервер прислал в push-уведомлении if (firstName.isEmpty) { firstName = cleanField(message.data['firstname']?.toString()); } if (lastName.isEmpty) { lastName = cleanField(message.data['lastname']?.toString()); } // Собираем полное имя String title = '$firstName $lastName'.trim(); // 3. Если имени нет ни в кэше, ни в полях сервера, берем username if (title.isEmpty) { title = cleanField(message.data['username']?.toString()); } // 4. Если вообще всё было "unknown" или пусто — ставим заглушку if (title.isEmpty) { title = 'Без имени'; } // ========================================================== // 4. Показываем локальное уведомление final String groupKey = 'ru.chepuhagram.app.$senderId'; await flutterLocalNotificationsPlugin.show( id: senderId ?? 0, title: '', body: '', notificationDetails: NotificationDetails( android: AndroidNotificationDetails( 'Messages', 'Новые сообщения', groupKey: groupKey, setAsGroupSummary: true, importance: Importance.high, priority: Priority.high, groupAlertBehavior: GroupAlertBehavior.all, ), ), ); await flutterLocalNotificationsPlugin.show( id: message.hashCode, title: title, // Отвалидированное имя с сервера или из кэша body: notificationText, notificationDetails: NotificationDetails( android: AndroidNotificationDetails( 'chat_id', 'Messages', groupKey: groupKey, importance: Importance.high, priority: Priority.high, showWhen: true, ), ), payload: jsonEncode({ 'type': 'enc_message', 'sender_id': message.data['sender_id'], 'timestamp': message.data['timestamp'] ?? DateTime.now().toIso8601String(), }), ); 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 StatefulWidget { const MyApp({super.key}); @override State createState() => _MyAppState(); } class _MyAppState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { // Закрываем сокет, как только приложение сворачивается. if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive || state == AppLifecycleState.detached) { try { //context.read().closeRealtime(); } catch (_) {} return; } // На возврате в приложение — пробуем переподключиться (если есть токен). if (state == AppLifecycleState.resumed) { try { context.read().initRealtime(); } catch (_) {} } } @override Widget build(BuildContext context) { final themeProvider = context.watch(); return MaterialApp( title: 'Chepuhagram', debugShowCheckedModeBanner: false, themeAnimationDuration: const Duration(milliseconds: 300), themeAnimationCurve: Curves.easeInOut, theme: themeProvider.themeData, themeMode: themeProvider.themeMode, navigatorKey: navigatorKey, navigatorObservers: [routeObserver], routes: {'/splash': (context) => const SplashScreen()}, initialRoute: '/splash', ); } }