import 'dart:convert'; import 'package:chepuhagram/core/constants.dart'; import 'package:chepuhagram/domain/services/api_service.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../screens/settings_screen.dart'; import '../screens/new_chat_screen.dart'; import '../screens/chat_screen.dart'; import 'my_profile_screen.dart'; import '/logic/contact_provider.dart'; import '/logic/auth_provider.dart'; import 'package:firebase_core/firebase_core.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'; import 'dart:async'; import 'package:http/http.dart' as http; import 'package:package_info_plus/package_info_plus.dart'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:path_provider/path_provider.dart'; import 'package:open_filex/open_filex.dart'; import '/data/datasources/ws_client.dart'; import '/data/models/contact_model.dart'; import 'user_profile_screen.dart'; class ContactsScreen extends StatefulWidget { final int? targetChatId; const ContactsScreen({super.key, this.targetChatId}); @override State createState() => _ContactsScreenState(); } class _ContactsScreenState extends State with RouteAware { static const String _notificationLaunchKey = 'notification_launch_data'; StreamSubscription? _socketSubscription; bool _isDownloading = false; double _downloadProgress = 0.0; int _downloadedBytes = 0; int _downloadTotalBytes = 0; int _apkFileSizeBytes = 0; CancelToken? _cancelToken = CancelToken(); String? _latestApkUrl; bool _showUpdateBanner = false; bool _contactsLoaded = false; Timer? _contactLoadTimer; Contact? _selectedContact; Contact? _profileContact; double _contactsPaneWidth = 290; double _profilePaneWidth = 360; final double _collapsedContactsWidth = 80; final double _minExpandedContactsWidth = 290; final double _maxExpandedContactsWidth = 500; double _dragStartWidth = 0; // Адаптивное состояние навигации int _currentIndex = 0; bool _isLeftRailExpanded = false; // Хранилище стабильно загруженных локальных имён Map _localFullNames = {}; @override void initState() { super.initState(); print('ContactsScreen initState, targetChatId: ${widget.targetChatId}'); _setupPushNotifications(); final socketService = Provider.of(context, listen: false); _socketSubscription = socketService.messages.listen(_handleIncomingMessage); WidgetsBinding.instance.addPostFrameCallback((_) { final authProvider = context.read(); final contactProvider = context.read(); print( 'Setting current user ID in ContactProvider: ${authProvider.currentUserId}', ); contactProvider.setCurrentUserId(authProvider.currentUserId); _startContactsLoadTimer(); }); } // Метод стабильной потокобезопасной подгрузки локальных имён из кэша Future _loadLocalNames() async { final prefs = await SharedPreferences.getInstance(); final contactProvider = context.read(); final Map tempNames = {}; for (var contact in contactProvider.contacts) { final String? fName = prefs.getString('firstname_${contact.id}'); final String? lName = prefs.getString('lastname_${contact.id}'); if (fName != null || lName != null) { tempNames[contact.id] = '${fName ?? ''} ${lName ?? ''}'.trim(); } } if (mounted) { setState(() { _localFullNames = tempNames; }); } } Future _startContactsLoadTimer() async { if (_contactLoadTimer != null && _contactLoadTimer!.isActive) return; _contactLoadTimer = Timer(const Duration(seconds: 2), () { _initContacts(); }); } Future _initContacts() async { if (_contactsLoaded) return; final contactProvider = context.read(); await contactProvider.loadContacts(); await _loadLocalNames(); // Гарантированный вызов после загрузки контактов print('Contacts loaded, checking targetChatId: ${widget.targetChatId}'); WidgetsBinding.instance.addPostFrameCallback((_) { _checkAppUpdate(); }); if (widget.targetChatId != null) { _navigateToTargetChat(); } else { _checkSavedNotificationTarget(); } _contactLoadTimer?.cancel(); _contactLoadTimer = null; _contactsLoaded = true; } @override void didChangeDependencies() { super.didChangeDependencies(); routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute); } @override void didPopNext() async { print("Пользователь вернулся на этот экран!"); await _refreshData(); } @override void dispose() { routeObserver.unsubscribe(this); _socketSubscription?.cancel(); super.dispose(); } Future _refreshData() async { print("Обновляем данные контактов и сообщений..."); final contactProvider = context.read(); await contactProvider.loadContacts(); await _loadLocalNames(); // Синхронизируем локальные имена при возврате } Future _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; print('Recovered saved notification data: $data'); final senderId = int.tryParse(data['sender_id']?.toString() ?? ''); final type = data['type']?.toString(); 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(); try { final contact = contactProvider.contacts.firstWhere( (c) => c.id == targetChatId, ); print('Auto-navigating to chat with contact: ${contact.username}'); _selectContact(contact); } catch (e) { print('Target contact with id $targetChatId not found: $e'); } } bool _isMobileLayout(BuildContext context) { return MediaQuery.of(context).size.width < 700; } bool _isTabletLayout(BuildContext context) { final width = MediaQuery.of(context).size.width; return width >= 700 && width < 1000; } bool _isDesktopLayout(BuildContext context) { return MediaQuery.of(context).size.width >= 1000; } void _selectContact(Contact contact) { setState(() { _selectedContact = contact; if (_profileContact != null && _isDesktopLayout(context)) { _profileContact = contact; } currentActiveChatContactId = contact.id; }); } void _openProfile(Contact contact) { if (_isDesktopLayout(context)) { setState(() { _profileContact = contact; }); return; } if (_isTabletLayout(context)) { showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) => SizedBox( height: MediaQuery.of(context).size.height * 0.85, child: UserProfileScreen( userId: contact.id, username: contact.username, name: contact.name, ), ), ); return; } Navigator.push( context, MaterialPageRoute( builder: (_) => UserProfileScreen( userId: contact.id, username: contact.username, name: contact.name, ), ), ); } void _clearSelectedContact() { setState(() { _selectedContact = null; if (!_isDesktopLayout(context)) { _profileContact = null; } currentActiveChatContactId = null; }); } Widget _buildPlaceholder(String text) { return Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Text( text, textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleLarge?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ), ); } Widget _buildContactsPane() { return Consumer( builder: (context, contactProvider, child) { if (contactProvider.isLoading) { return const Center(child: CircularProgressIndicator()); } if (contactProvider.error != null) { return Center( child: Text( '${contactProvider.error?.replaceAll('Exception: ', '')}', style: TextStyle(color: Theme.of(context).colorScheme.error), textAlign: TextAlign.center, ), ); } final isCollapsed = !_isMobileLayout(context) && (_contactsPaneWidth <= _collapsedContactsWidth); if (contactProvider.contacts.isEmpty) { return _buildPlaceholder( 'Список чатов пуст. Нажмите карандаш, чтобы начать.', ); } return ListView.separated( physics: const BouncingScrollPhysics(), itemCount: contactProvider.contacts.length, separatorBuilder: (context, index) => Divider( height: 1, indent: isCollapsed ? 12 : 84, endIndent: 12, color: Theme.of( context, ).colorScheme.outlineVariant.withOpacity(0.15), ), itemBuilder: (context, index) { final contact = contactProvider.contacts[index]; final colorScheme = Theme.of(context).colorScheme; final isSelected = _selectedContact?.id == contact.id; final localName = _localFullNames[contact.id]; final displayName = (localName != null && localName.isNotEmpty) ? localName : '${contact.name != 'Unknown' ? contact.name : ''} ${contact.surname != 'Unknown' ? contact.surname : ''}'.trim(); final contactInitials = displayName.isNotEmpty ? displayName .trim() .split(RegExp(r'\s+')) .take(2) .map((e) => e[0].toUpperCase()) .join() : '?'; String timeText = ''; if (contact.lastMessageTime != null) { final localTime = contact.lastMessageTime!.toLocal(); timeText = '${localTime.hour.toString().padLeft(2, '0')}:${localTime.minute.toString().padLeft(2, '0')}'; } final bool isLastMessageEmpty = contact.lastMessage != null && contact.lastMessage!.trim().isEmpty; final String displayLastMessage = isLastMessageEmpty ? 'Вложение' : (contact.lastMessage ?? 'Нет сообщений'); final Color lastMessageColor = (contact.lastMessage != null && !isLastMessageEmpty) ? colorScheme.onSurfaceVariant : colorScheme.outline.withOpacity(1); return Padding( // ФИКС ОВЕРФЛОУ: уменьшаем внешний отступ при сжатии до 4px (было 8) padding: EdgeInsets.symmetric( horizontal: isCollapsed ? 4 : 8, vertical: 2, ), child: InkWell( onTap: () => _selectContact(contact), borderRadius: BorderRadius.circular(16), child: AnimatedContainer( duration: const Duration(milliseconds: 150), // ФИКС ОВЕРФЛОУ: уменьшаем внутренний отступ при сжатии до 6px (было 12) padding: EdgeInsets.symmetric( horizontal: isCollapsed ? 6 : 12, vertical: 10, ), decoration: BoxDecoration( color: isSelected ? colorScheme.primaryContainer.withOpacity(0.4) : Colors.transparent, borderRadius: BorderRadius.circular(16), ), child: Row( // Центрируем аватарку по оси, когда колонка зажата mainAxisAlignment: isCollapsed ? MainAxisAlignment.center : MainAxisAlignment.start, children: [ Stack( clipBehavior: Clip.none, children: [ Container( width: 52, height: 52, decoration: BoxDecoration( shape: BoxShape.circle, color: colorScheme.primaryContainer, ), child: ClipOval( child: Stack( alignment: Alignment.center, children: [ Text( contactInitials, style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: colorScheme.onPrimaryContainer, ), ), if (contact.avatarUrl != null) Image.network( contact.avatarUrl!, fit: BoxFit.cover, width: 52, height: 52, errorBuilder: (context, error, stackTrace) => const SizedBox.shrink(), loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return const SizedBox.shrink(); }, ), ], ), ), ), if (contact.isOnline) Positioned( right: -1, bottom: -1, child: Container( width: 14, height: 14, decoration: BoxDecoration( color: Colors.green.shade500, shape: BoxShape.circle, border: Border.all( color: isSelected ? Color.alphaBlend( colorScheme.primaryContainer .withOpacity(0.4), colorScheme.background, ) : colorScheme.background, width: 2.5, ), ), ), ), ], ), if (!isCollapsed) ...[ const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( displayName, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: -0.3, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), if (timeText.isNotEmpty) Text( timeText, style: TextStyle( color: colorScheme.outline, fontSize: 12, fontWeight: FontWeight.w500, ), ), ], ), const SizedBox(height: 5), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( displayLastMessage, style: TextStyle( color: lastMessageColor, fontSize: 14, fontStyle: isLastMessageEmpty ? FontStyle.italic : FontStyle.normal, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), if (contact.unreadCount != null && contact.unreadCount! > 0) Container( padding: const EdgeInsets.symmetric( horizontal: 7, vertical: 3, ), decoration: BoxDecoration( color: colorScheme.primary, borderRadius: BorderRadius.circular(10), ), constraints: const BoxConstraints( minWidth: 20, ), child: Text( '${contact.unreadCount}', style: const TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), ), ], ), ], ), ), ], ], ), ), ), ); }, ); }, ); } Widget _buildChatPane() { if (_selectedContact == null) { return _buildPlaceholder('Выберите чат слева, чтобы начать переписку.'); } return ChatScreen( key: ValueKey(_selectedContact!.id), contact: _selectedContact!, onOpenProfile: _openProfile, onBack: _clearSelectedContact, showBackButton: false, ); } Widget _buildProfilePane() { final contact = _profileContact; if (contact == null) { return _buildPlaceholder('Профиль выбранного пользователя будет здесь.'); } return UserProfileScreen( key: ValueKey(contact.id), userId: contact.id, username: contact.username, name: contact.name, onClose: () { setState(() { _profileContact = null; }); }, ); } Widget _buildContactsListWithScaffold(bool isPhone) { final colorScheme = Theme.of(context).colorScheme; final isCollapsed = !isPhone && (_contactsPaneWidth <= _collapsedContactsWidth); Widget bodyWidget; String titleText = "Chepuhagram"; bool showSearch = true; if (isPhone) { switch (_currentIndex) { case 1: titleText = "Профиль"; showSearch = false; bodyWidget = const MyProfileScreen(isFromList: true); break; case 2: titleText = "Настройки"; showSearch = false; bodyWidget = const SettingsScreen(isFromList: true); break; case 0: default: titleText = "Chepuhagram"; showSearch = true; bodyWidget = _buildContactsPane(); break; } } else { bodyWidget = _buildContactsPane(); } return Scaffold( backgroundColor: isPhone ? colorScheme.background : Colors.transparent, appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, scrolledUnderElevation: 0, title: Text( titleText, style: const TextStyle( fontWeight: FontWeight.w800, fontSize: 24, letterSpacing: -0.5, ), ), centerTitle: false, actions: [ if (showSearch) Padding( padding: const EdgeInsets.only(right: 16.0), child: ClipOval( child: Material( child: IconButton( icon: const Icon(Icons.search_rounded, size: 22), onPressed: () {}, ), ), ), ), ], ), body: Column( children: [ Expanded(child: bodyWidget), if (_showUpdateBanner) SafeArea(top: false, child: _buildUpdateBanner(isPhone)), ], ), floatingActionButton: (isCollapsed || (isPhone && _currentIndex != 0)) ? null : AnimatedPadding( duration: const Duration(milliseconds: 150), curve: Curves.easeInOut, padding: EdgeInsets.only( bottom: _showUpdateBanner ? _isDownloading ? 150.0 : 100.0 : 16.0, ), child: FloatingActionButton( onPressed: () => Navigator.push( context, MaterialPageRoute(builder: (_) => const NewChatScreen()), ), child: const Icon(Icons.edit_note_rounded), ), ), bottomNavigationBar: isPhone ? BottomNavigationBar( currentIndex: _currentIndex, elevation: 8, onTap: (index) => setState(() => _currentIndex = index), items: const [ BottomNavigationBarItem( icon: Icon(Icons.chat_bubble_outline_rounded), activeIcon: Icon(Icons.chat_bubble_rounded), label: "Чаты", ), BottomNavigationBarItem( icon: Icon(Icons.person_outline_rounded), activeIcon: Icon(Icons.person_rounded), label: "Профиль", ), BottomNavigationBarItem( icon: Icon(Icons.settings_outlined), activeIcon: Icon(Icons.settings_rounded), label: "Настройки", ), ], ) : null, ); } Widget _buildUpdateBanner(bool isPhone) { final isCollapsed = !isPhone && (_contactsPaneWidth <= _collapsedContactsWidth); if (isCollapsed) return const SizedBox.shrink(); return Container( // Сделали аккуратные отступы сверху для баннера margin: const EdgeInsets.fromLTRB(16, 4, 16, 12), child: Material( elevation: 6, borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.orange.shade600, Colors.deepOrange.shade400], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Expanded( child: Text( _isDownloading ? 'Скачивание ${(_downloadProgress * 100).toStringAsFixed(0)}%' : _apkFileSizeBytes > 0 ? 'Доступно новое обновление: ${_formatBytes(_apkFileSizeBytes)}' : 'Доступно новое обновление!', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), TextButton( onPressed: () async { if (_isDownloading) { _cancelToken?.cancel("Пользователь отменил загрузку"); setState(() { _isDownloading = false; _downloadProgress = 0.0; }); } else { setState(() { _isDownloading = true; _cancelToken = CancelToken(); }); await _startDownload(); } }, style: TextButton.styleFrom( backgroundColor: Colors.white24, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: Text( _isDownloading ? "Отмена" : "Обновить", style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), ), ], ), if (_isDownloading) ...[ const SizedBox(height: 12), LinearProgressIndicator( value: _downloadProgress, color: Colors.white, backgroundColor: Colors.white24, ), const SizedBox(height: 8), Align( alignment: Alignment.centerLeft, child: Text( '${_formatBytes(_downloadedBytes)} из ${_formatBytes(_downloadTotalBytes)}', style: const TextStyle(color: Colors.white70, fontSize: 14), ), ), ], ], ), ), ), ); } Widget _buildWindowsNavigationRail() { final double railWidth = _isLeftRailExpanded ? 220 : 68; final colorScheme = Theme.of(context).colorScheme; return AnimatedContainer( duration: const Duration(milliseconds: 250), curve: Curves.fastOutSlowIn, width: railWidth, color: colorScheme.surfaceVariant.withOpacity(0.25), child: Column( children: [ const SizedBox(height: 12), IconButton( icon: Icon( _isLeftRailExpanded ? Icons.menu_open_rounded : Icons.menu_rounded, ), onPressed: () => setState(() => _isLeftRailExpanded = !_isLeftRailExpanded), ), const Divider(height: 24, indent: 12, endIndent: 12), _buildRailItem( Icons.chat_bubble_outline_rounded, Icons.chat_bubble_rounded, "Чаты", 0, ), _buildRailItem( Icons.settings_outlined, Icons.settings_rounded, "Настройки", 2, ), _buildRailItem( Icons.person_outline_rounded, Icons.person_rounded, "Профиль", 1, ), ], ), ); } Widget _buildRailItem( IconData icon, IconData activeIcon, String label, int index, ) { final isSelected = _currentIndex == index; final colorScheme = Theme.of(context).colorScheme; final color = isSelected ? colorScheme.primary : colorScheme.onSurface; return Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: InkWell( onTap: () => setState(() => _currentIndex = index), borderRadius: BorderRadius.circular(12), child: Container( height: 48, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: isSelected ? colorScheme.primary.withOpacity(0.08) : Colors.transparent, borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: _isLeftRailExpanded ? MainAxisAlignment.start : MainAxisAlignment.center, children: [ Icon(isSelected ? activeIcon : icon, color: color, size: 22), if (_isLeftRailExpanded) ...[ const SizedBox(width: 16), Expanded( child: Text( label, style: TextStyle( color: color, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, fontSize: 14, ), overflow: TextOverflow.ellipsis, ), ), ], ], ), ), ), ); } Widget _buildResizableDivider({ required Function(DragUpdateDetails) onPanUpdate, Function(DragStartDetails)? onPanStart, }) { return GestureDetector( onPanStart: onPanStart, onPanUpdate: onPanUpdate, child: MouseRegion( cursor: SystemMouseCursors.resizeColumn, child: Container( width: 8, color: Colors.transparent, alignment: Alignment.center, child: VerticalDivider( width: 1, thickness: 1, color: Theme.of(context).dividerColor.withOpacity(0.4), ), ), ), ); } Widget _buildResponsiveBody(bool isPhone) { final media = MediaQuery.of(context); // Проверка физического форм-фактора (надежнее, чем Platform.isAndroid) final bool isPhoneFormFactor = media.size.shortestSide < 600; // 1. ЛОГИКА ДЛЯ СМАРТФОНОВ (любая ОС, если экран маленький) if (isPhoneFormFactor) { if (_selectedContact != null) { return ChatScreen( contact: _selectedContact!, onOpenProfile: _openProfile, onBack: _clearSelectedContact, showBackButton: true, ); } return _buildContactsListWithScaffold(true); } // 2. ЛОГИКА ДЛЯ ПЛАНШЕТОВ И КОМПЬЮТЕРОВ (Широкие экраны) Widget centerPane; switch (_currentIndex) { case 1: centerPane = const Expanded(child: MyProfileScreen(isFromList: false)); break; case 2: centerPane = const Expanded(child: SettingsScreen(isFromList: false)); break; case 0: default: centerPane = Expanded( child: Row( children: [ SizedBox( width: _contactsPaneWidth, child: _buildContactsListWithScaffold(false), ), _buildResizableDivider( onPanStart: (details) => _dragStartWidth = _contactsPaneWidth, onPanUpdate: (details) { setState(() { final newWidth = details.globalPosition.dx; if (_dragStartWidth > _collapsedContactsWidth) { if (newWidth < (_minExpandedContactsWidth / 2)) { _contactsPaneWidth = _collapsedContactsWidth; } else { _contactsPaneWidth = newWidth.clamp( _minExpandedContactsWidth, _maxExpandedContactsWidth, ); } } else { if (newWidth > (_minExpandedContactsWidth / 2) + 40) { _contactsPaneWidth = newWidth.clamp( _minExpandedContactsWidth, _maxExpandedContactsWidth, ); } } }); }, ), Expanded(child: _buildChatPane()), if (_profileContact != null && _isDesktopLayout(context)) ...[ _buildResizableDivider( onPanUpdate: (details) { setState(() { final screenWidth = MediaQuery.of(context).size.width; _profilePaneWidth = (screenWidth - details.globalPosition.dx).clamp( 280, 500, ); }); }, ), SizedBox(width: _profilePaneWidth, child: _buildProfilePane()), ], ], ), ); break; } return Scaffold( body: Row(children: [_buildWindowsNavigationRail(), centerPane]), ); } @override Widget build(BuildContext context) { final bool isPhoneFormFactor = MediaQuery.of(context).size.shortestSide < 600; return PopScope( canPop: _selectedContact == null || !isPhoneFormFactor, onPopInvokedWithResult: (didPop, result) { if (didPop) return; if (_selectedContact != null && isPhoneFormFactor) { _clearSelectedContact(); } }, child: _buildResponsiveBody(isPhoneFormFactor), ); } Future _checkAppUpdate() async { print('Проверка обновлений'); PackageInfo packageInfo = await PackageInfo.fromPlatform(); try { final response = await http.get( Uri.parse('${AppConstants.baseUrl}/check-update'), ); if (response.statusCode == 200) { final data = jsonDecode(response.body); final String latestVersion = data['latest_version']; if (latestVersion != packageInfo.version) { setState(() { _showUpdateBanner = true; _latestApkUrl = data['apk_url']; }); if (_latestApkUrl != null) { final size = await _fetchApkSize(_latestApkUrl!); if (mounted) { setState(() => _apkFileSizeBytes = size); } } } } } catch (e) { print("Ошибка проверки обновлений: $e"); } } Future _setupPushNotifications() async { try { if (Firebase.apps.isEmpty) { print('Firebase is not initialized, skipping push notification setup.'); return; } await FirebaseMessaging.instance.requestPermission(); String? token = await FirebaseMessaging.instance.getToken(); if (token != null) { ApiService apiService = ApiService(); await apiService.updateFcmToken(token); } FirebaseMessaging.instance.onTokenRefresh.listen((newToken) { ApiService apiService = ApiService(); apiService.updateFcmToken(newToken); }); FirebaseMessaging.onMessage.listen(_handleIncomingMessage); FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { if (message.data['type'] == 'enc_message') { final senderId = int.tryParse( message.data['sender_id']?.toString() ?? '', ); if (senderId != null) _navigateToChatFromNotification(senderId); } }); } catch (e) { print('Push notification setup failed: $e'); } } void _navigateToChatFromNotification(int senderId) { final contactProvider = context.read(); if (contactProvider.contacts.isEmpty) { Future.delayed(const Duration(milliseconds: 500), () { if (mounted) _navigateToChatFromNotification(senderId); }); return; } try { final contact = contactProvider.contacts.firstWhere( (c) => c.id == senderId, ); _selectContact(contact); } catch (_) {} } Future _handleIncomingMessage(dynamic data) async { if (data is RemoteMessage) { await _handleFCMMessage(data); } else if (data is Map) { print('WebSocket message received in ContactsScreen: $data'); final contactProvider = context.read(); if (data['type'] == 'user_updated') { final userId = int.tryParse(data['user_id']?.toString() ?? ''); if (userId != null) { await contactProvider.updateContact(userId); await _loadLocalNames(); // Синхронно обновляем кэш имен на сокет } } if (data['type'] == 'user_online') { final userId = int.tryParse(data['user_id']?.toString() ?? ''); if (userId != null) { contactProvider.updateContactOnlineStatus(userId, true); if (mounted) { setState(() { if (_selectedContact != null && _selectedContact!.id == userId) { _selectedContact = contactProvider.contacts.firstWhere( (c) => c.id == userId, orElse: () => _selectedContact!, ); } }); } } } if (data['type'] == 'user_offline') { final userId = int.tryParse(data['user_id']?.toString() ?? ''); if (userId != null) { contactProvider.updateContactOnlineStatus(userId, false); if (mounted) { setState(() { if (_selectedContact != null && _selectedContact!.id == userId) { _selectedContact = contactProvider.contacts.firstWhere( (c) => c.id == userId, orElse: () => _selectedContact!, ); } }); } } } if (data['type'] == 'message_edited') { final messageId = int.tryParse(data['message_id']?.toString() ?? ''); final senderId = int.tryParse(data['sender_id']?.toString() ?? ''); if (messageId != null && senderId != null) { final contact = contactProvider.contacts .where((c) => c.id == senderId) .firstOrNull; if (contact != null) { final editedAt = DateTime.tryParse( data['edited_at']?.toString() ?? '', ); String lastMessageText = contact.lastMessage ?? ''; bool isDecrypted = false; final myPrivKey = await CryptoService().getPrivateKey(); if (myPrivKey != null && contact.publicKey != null) { try { final sharedSecret = await CryptoService().deriveSharedSecret( myPrivKey, contact.publicKey!, ); lastMessageText = await CryptoService().decryptMessage( data['content']?.toString() ?? '', sharedSecret, ); isDecrypted = true; } catch (_) {} } await contactProvider.updateContactLastMessage( contact.id, lastMessage: lastMessageText, lastMessageTime: editedAt, isLastMsgDecrypted: isDecrypted, lastMessageId: messageId, isEdited: true, ); } } } if (data['type'] == 'message_deleted') { final messageId = int.tryParse(data['message_id']?.toString() ?? ''); if (messageId != null) { final contactIndex = contactProvider.contacts.indexWhere( (c) => c.lastMessageId == messageId, ); if (contactIndex != -1) { await contactProvider.refreshContactLastMessage( contactProvider.contacts[contactIndex].id, ); } } } } } Future _handleFCMMessage(RemoteMessage message) async { try { final senderId = int.tryParse( message.data['sender_id']?.toString() ?? '', ); if (senderId != null && currentActiveChatContactId == senderId) return; const AndroidNotificationChannel channel = AndroidNotificationChannel( '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) return; final sharedSecret = await crypto.deriveSharedSecret( myPrivKey, message.data['public_key'], ); final decryptedText = await crypto.decryptMessage( message.data['content'], sharedSecret, ); if (senderId == null) return; final String groupKey = 'ru.chepuhagram.app.$senderId'; final prefs = await SharedPreferences.getInstance(); final String? firstName = prefs.getString( 'firstname_${message.data['sender_id']}', ); final String? lastName = prefs.getString( 'lastname_${message.data['sender_id']}', ); final String localFullName = '${firstName ?? ''} ${lastName ?? ''}' .trim(); final String title = localFullName.isNotEmpty ? localFullName : (message.data['username'] ?? 'Unknown'); await flutterLocalNotificationsPlugin.show( id: senderId, 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: decryptedText, notificationDetails: NotificationDetails( android: AndroidNotificationDetails( '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(), }), ); if (message.data['type'] == 'enc_message') { context.read().updateContact( senderId, lastMessage: decryptedText, lastMessageTime: DateTime.tryParse( message.data['timestamp'] ?? DateTime.now().toIso8601String(), ), isLastMsgDecrypted: true, unreadCount: message.data['unread_count'] != null ? int.tryParse(message.data['unread_count'].toString()) : null, ); } } catch (e) { print('Error processing foreground FCM: $e'); } } Future _startDownload() async { if (_latestApkUrl == null) return; setState(() => _isDownloading = true); Directory? dir = await getExternalStorageDirectory(); final path = '${dir!.path}/update.apk'; final file = File(path); if (await file.exists()) await file.delete(); try { setState(() { _downloadProgress = 0.0; _downloadedBytes = 0; _downloadTotalBytes = 0; }); await Dio().download( _latestApkUrl!, path, cancelToken: _cancelToken, onReceiveProgress: (rec, total) { if (mounted) { setState(() { _downloadedBytes = rec; _downloadTotalBytes = total > 0 ? total : 0; _downloadProgress = total > 0 ? rec / total : 0.0; }); } }, ); await OpenFilex.open(path); } catch (_) { } finally { if (mounted) setState(() { _isDownloading = false; _downloadProgress = 0.0; }); } } Future _fetchApkSize(String url) async { try { final response = await http.head(Uri.parse(url)); return int.tryParse(response.headers['content-length'] ?? '') ?? 0; } catch (_) { return 0; } } String _formatBytes(int bytes) { if (bytes <= 0) return '0 B'; const kb = 1024; const mb = kb * 1024; if (bytes < kb) return '$bytes B'; if (bytes < mb) return '${(bytes / kb).toStringAsFixed(1)} KB'; return '${(bytes / mb).toStringAsFixed(1)} MB'; } }