376 lines
13 KiB
Dart
376 lines
13 KiB
Dart
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';
|
||
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 'package:chepuhagram/data/datasources/ws_client.dart';
|
||
import 'dart:async';
|
||
|
||
class ContactsScreen extends StatefulWidget {
|
||
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';
|
||
StreamSubscription<dynamic>? _socketSubscription;
|
||
|
||
@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().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'],
|
||
'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 message: $e');
|
||
}
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_socketSubscription?.cancel();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
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: 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: () {
|
||
Navigator.push(
|
||
context,
|
||
MaterialPageRoute(
|
||
builder: (_) => ChatScreen(contact: contact),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
},
|
||
);
|
||
},
|
||
),
|
||
floatingActionButton: FloatingActionButton(
|
||
onPressed: () {
|
||
Navigator.push(
|
||
context,
|
||
MaterialPageRoute(builder: (_) => const NewChatScreen()),
|
||
);
|
||
},
|
||
child: Icon(Icons.edit, color: Theme.of(context).colorScheme.onSurface),
|
||
),
|
||
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),
|
||
accountEmail: Text(
|
||
username == null || username.isEmpty ? '' : '@$username',
|
||
),
|
||
currentAccountPicture: CircleAvatar(
|
||
backgroundColor: Theme.of(context).colorScheme.onSurface,
|
||
child: Text(
|
||
initials.isEmpty ? 'U' : initials,
|
||
style: TextStyle(
|
||
fontSize: 20,
|
||
fontWeight: FontWeight.bold,
|
||
color: Theme.of(context).colorScheme.primaryContainer,
|
||
),
|
||
),
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: Theme.of(context).colorScheme.primaryContainer,
|
||
),
|
||
);
|
||
},
|
||
),
|
||
ListTile(
|
||
leading: const Icon(Icons.settings),
|
||
title: const Text("Настройки"),
|
||
onTap: () {
|
||
// Закрываем Drawer и переходим на экран настроек
|
||
Navigator.pop(context);
|
||
Navigator.push(
|
||
context,
|
||
MaterialPageRoute(builder: (_) => const SettingsScreen()),
|
||
);
|
||
},
|
||
),
|
||
ListTile(
|
||
leading: const Icon(Icons.info_outline),
|
||
title: const Text("О приложении"),
|
||
onTap: () {
|
||
/* ... */
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|