Chepuhagram/lib/presentation/screens/contacts_screen.dart

690 lines
24 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 '../widgets/contact_tile.dart';
import '../screens/settings_screen.dart';
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';
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';
class ContactsScreen extends StatefulWidget {
final int? targetChatId;
const ContactsScreen({super.key, this.targetChatId});
@override
State<ContactsScreen> createState() => _ContactsScreenState();
}
class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
static const String _notificationLaunchKey = 'notification_launch_data';
StreamSubscription<dynamic>? _socketSubscription;
bool _isDownloading = false;
double _downloadProgress = 0.0;
CancelToken? _cancelToken = CancelToken();
String? _latestApkUrl;
bool _showUpdateBanner = false;
@override
void initState() {
super.initState();
print('ContactsScreen initState, targetChatId: ${widget.targetChatId}');
_setupPushNotifications();
final socketService = Provider.of<SocketService>(context, listen: false);
_socketSubscription = socketService.messages.listen(_handleIncomingMessage);
WidgetsBinding.instance.addPostFrameCallback((_) {
final authProvider = context.read<AuthProvider>();
final contactProvider = context.read<ContactProvider>();
// Установить текущего пользователя и загрузить контакты с сообщениями
print(
'Setting current user ID in ContactProvider: ${authProvider.currentUserId}',
);
contactProvider.setCurrentUserId(authProvider.currentUserId);
_initContacts();
});
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkAppUpdate();
});
}
Future<void> _initContacts() async {
final contactProvider = context.read<ContactProvider>();
// Ждем завершения загрузки контактов
await contactProvider.loadContacts();
print('Contacts loaded, checking targetChatId: ${widget.targetChatId}');
// Дальнейшая логика выполнится только после того, как loadContacts завершится
if (widget.targetChatId != null) {
_navigateToTargetChat();
} else {
_checkSavedNotificationTarget();
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
}
@override
void didPopNext() {
print("Пользователь вернулся на этот экран!");
_refreshData();
}
@override
void dispose() {
routeObserver.unsubscribe(this);
_socketSubscription?.cancel();
super.dispose();
}
void _refreshData() {
print("Обновляем данные контактов и сообщений...");
final contactProvider = context.read<ContactProvider>();
contactProvider.loadContacts();
}
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) async {
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; // Устанавливаем активный чат
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (_) => ChatScreen(contact: contact)),
);
if (result != null) {
_refreshData(); // Обновляем данные при возвращении с чата, если нужно
}
} catch (e) {
print('Target contact with id $targetChatId not found: $e');
}
}
Future<void> _checkAppUpdate() async {
print('Проверка обновлений');
PackageInfo packageInfo = await PackageInfo.fromPlatform();
try {
// 1. Запрос к вашему FastAPI
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'];
print('444444');
print(latestVersion);
print(packageInfo.version);
// Сравнение версий (предположим, у вас есть способ получить текущую версию)
if (latestVersion != packageInfo.version) {
setState(() {
_showUpdateBanner = true;
_latestApkUrl = data['apk_url'];
});
}
}
} catch (e) {
print("Ошибка проверки обновлений: $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(_handleIncomingMessage);
// 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) async {
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; // Устанавливаем активный чат
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (_) => ChatScreen(contact: contact)),
);
if (result != null) {
_refreshData(); // Обновляем данные при возвращении с чата, если нужно
}
} catch (e) {
// Contact not found, stay on contacts screen
print('Contact not found for notification: $senderId');
}
}
Future<void> _handleIncomingMessage(dynamic data) async {
if (data is RemoteMessage) {
// FCM message
await _handleFCMMessage(data);
} else if (data is Map<String, dynamic>) {
// WebSocket message
print('WebSocket message received: $data');
if (data['type'] == 'user_updated') {
final userId = int.tryParse(data['user_id']?.toString() ?? '');
if (userId != null) {
final contactProvider = context.read<ContactProvider>();
contactProvider.updateContact(userId);
}
}
}
}
Future<void> _handleFCMMessage(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(
'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,
);
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');
// Show local notification
await flutterLocalNotificationsPlugin.show(
senderId,
'',
'',
NotificationDetails(
android: AndroidNotificationDetails(
'Messages',
'Новые сообщения',
groupKey: groupKey,
setAsGroupSummary: true,
importance: Importance.high,
priority: Priority.high,
groupAlertBehavior: GroupAlertBehavior.all,
),
),
);
await flutterLocalNotificationsPlugin.show(
message.hashCode,
title,
decryptedText,
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') {
final contactProvider = context.read<ContactProvider>();
contactProvider.loadContacts();
}
} catch (e) {
print('Error processing foreground FCM message: $e');
}
}
@override
Widget build(BuildContext context) {
final double fabBottomPadding = _showUpdateBanner ? 120.0 : 16.0;
return Scaffold(
appBar: AppBar(
title: Text(
"Chepuhagram",
style: TextStyle(fontWeight: FontWeight.bold),
),
centerTitle: false,
elevation: 0,
actions: [IconButton(icon: const Icon(Icons.search), onPressed: () {})],
),
body: Stack(
children: [
Consumer<ContactProvider>(
builder: (context, contactProvider, child) {
if (contactProvider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (contactProvider.error != null) {
return Center(child: Text('Error: ${contactProvider.error}'));
}
return ListView.separated(
itemCount: contactProvider.contacts.length,
separatorBuilder: (context, index) => Divider(
height: 1,
indent: 80,
color: Theme.of(context).colorScheme.primaryContainer,
),
itemBuilder: (context, index) {
final contact = contactProvider.contacts[index];
return ContactTile(
contact: contact,
onTap: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ChatScreen(contact: contact),
),
);
if (result != null) {
_refreshData(); // Обновляем данные при возвращении с чата, если нужно
}
},
);
},
);
},
),
if (_showUpdateBanner)
Positioned(
left: 0,
right: 0,
bottom: 40,
child: _buildUpdateBanner(),
),
],
),
floatingActionButton: AnimatedPadding(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
padding: EdgeInsets.only(bottom: fabBottomPadding),
child: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const NewChatScreen()),
);
},
child: const Icon(Icons.edit),
),
),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
// Шапка меню с данными юзера
Consumer<AuthProvider>(
builder: (context, authProvider, _) {
final username = authProvider.username;
final displayName = authProvider.displayName;
final initials =
(displayName.isNotEmpty ? displayName : (username ?? 'U'))
.trim()
.split(RegExp(r'\s+'))
.where((p) => p.isNotEmpty)
.take(2)
.map((p) => p[0].toUpperCase())
.join();
return UserAccountsDrawerHeader(
accountName: Text(
displayName,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
accountEmail: Text(
username == null || username.isEmpty ? '' : '@$username',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
currentAccountPicture: CircleAvatar(
backgroundColor: authProvider.avatarUrl == null && authProvider.avatarPath == null
? Theme.of(context).colorScheme.onSurface
: null,
backgroundImage: authProvider.avatarUrl != null
? NetworkImage(authProvider.avatarUrl!)
: authProvider.avatarPath != null
? FileImage(File(authProvider.avatarPath!))
: null,
child: (authProvider.avatarUrl == null && authProvider.avatarPath == null)
? Text(
initials.isEmpty ? 'U' : initials,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primaryContainer,
),
)
: null,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.inversePrimary,
),
);
},
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text("Настройки"),
onTap: () {
// Закрываем Drawer и переходим на экран настроек
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(builder: (_) => SettingsScreen()),
);
},
),
ListTile(
leading: const Icon(Icons.info_outline),
title: const Text("О приложении"),
onTap: () {
/* ... */
},
),
],
),
),
);
}
Future<void> _startDownload() async {
if (_latestApkUrl == null) return;
// Показываем индикатор
setState(() => _isDownloading = true);
final dir = await getExternalStorageDirectory();
final path = '${dir!.path}/update.apk';
final file = File(path);
// Удаляем старый файл, если он есть, чтобы гарантировать чистоту
if (await file.exists()) {
await file.delete();
}
try {
// Скачиваем файл «в лоб»
await Dio().download(
_latestApkUrl!,
path,
cancelToken: _cancelToken,
onReceiveProgress: (rec, total) {
if (total != -1) {
if (mounted) {
setState(() => _downloadProgress = rec / total);
}
}
},
);
// После успешного скачивания — установка
final result = await OpenFilex.open(path);
if (result.type != ResultType.done) {
print("Ошибка при установке: ${result.message}");
}
} on DioException catch (e) {
if (e.type != DioExceptionType.cancel) {
print("Ошибка скачивания: $e");
}
} catch (e) {
print("Ошибка: $e");
} finally {
if (mounted) {
setState(() => _isDownloading = false);
}
}
}
Widget _buildUpdateBanner() {
return Container(
margin: const EdgeInsets.fromLTRB(
12,
0,
12,
16,
), // Отступы от краев и снизу
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: [
const Icon(
Icons.system_update_alt,
color: Colors.white,
size: 28,
),
const SizedBox(width: 12),
Expanded(
child: Text(
_isDownloading
? 'Скачивание ${(_downloadProgress * 100).toStringAsFixed(0)}%'
: "Доступно новое обновление!",
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
TextButton(
onPressed: () async {
if (_isDownloading) {
// Если уже качаем — отменяем
_cancelToken?.cancel("Пользователь отменил загрузку");
setState(() {
_isDownloading = false;
_cancelToken = null; // Обязательно обнуляем токен!
_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,
),
],
],
),
),
),
);
}
}