import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart' as fc; import 'package:url_launcher/url_launcher.dart'; import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; import '/logic/auth_provider.dart'; import '/data/models/contact_model.dart'; import '/data/repositories/contact_repository.dart'; import 'chat_screen.dart'; import 'package:cached_network_image/cached_network_image.dart'; class NewChatScreen extends StatefulWidget { const NewChatScreen({super.key}); @override State createState() => _NewChatScreenState(); } class _NewChatScreenState extends State { bool _isLoading = true; bool _permissionDenied = false; String? _error; List> _registeredMatches = []; List _unregisteredMatches = []; @override void initState() { super.initState(); _requestPermissionAndLoad(); } String _normalizePhone(String p) { final digits = p.replaceAll(RegExp(r'\D'), ''); if (digits.length >= 10) { return digits.substring(digits.length - 10); } return digits; } Future _requestPermissionAndLoad() async { setState(() { _isLoading = true; _permissionDenied = false; _error = null; }); try { // 1. Проверяем разрешение на доступ к контактам (запрашиваем при необходимости) bool permission = await fc.FlutterContacts.requestPermission(readonly: true); if (!permission) { setState(() { _permissionDenied = true; _isLoading = false; }); return; } // 2. Разрешение получено, загружаем контакты устройства final deviceContacts = await fc.FlutterContacts.getContacts(withProperties: true); // 3. Загружаем список зарегистрированных пользователей из нашего репозитория final contactRepository = ContactRepository(); final registeredUsers = await contactRepository.fetchAllUsers(); // Диагностика для отладки сопоставления контактов debugPrint('--- NEW CHAT CONTACT MATCHING DEBUG ---'); debugPrint('Total registered users from server: ${registeredUsers.length}'); for (var u in registeredUsers) { debugPrint(' Server User: ID=${u.id}, Username=${u.username}, Phone="${u.phone}"'); } debugPrint('Total device contacts: ${deviceContacts.length}'); for (var dc in deviceContacts) { final phonesStr = dc.phones.map((p) => '${p.number} (norm: ${_normalizePhone(p.number)})').join(', '); debugPrint(' Device Contact: Name="${dc.displayName}", Phones=[$phonesStr]'); } debugPrint('---------------------------------------'); // 4. Группируем и сопоставляем контакты по последним 10 цифрам номера телефона final Map registeredMap = {}; for (var u in registeredUsers) { if (u.phone != null) { final norm = _normalizePhone(u.phone!); if (norm.isNotEmpty) { registeredMap[norm] = u; } } } final List> regMatches = []; final List unregMatches = []; for (var dc in deviceContacts) { bool matched = false; for (var phoneObj in dc.phones) { final norm = _normalizePhone(phoneObj.number); if (norm.isNotEmpty && registeredMap.containsKey(norm)) { final regUser = registeredMap[norm]!; regMatches.add({ 'registered': regUser, 'device': dc, }); matched = true; break; } } if (!matched && dc.phones.isNotEmpty) { unregMatches.add(dc); } } // Сортировка по имени в алфавитном порядке regMatches.sort((a, b) => (a['device'] as fc.Contact).displayName.toLowerCase().compareTo((b['device'] as fc.Contact).displayName.toLowerCase())); unregMatches.sort((a, b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase())); setState(() { _registeredMatches = regMatches; _unregisteredMatches = unregMatches; _isLoading = false; }); } catch (e) { setState(() { _error = e.toString(); _isLoading = false; }); } } Future _sendInvitation(String phoneNumber) async { final message = 'Привет! Присоединяйся к бета-тесту мессенджера Чепухаграм. Напиши разработчику в Telegram @ArturKarasevich для получения доступа!'; final uri = Uri.parse('sms:$phoneNumber?body=${Uri.encodeComponent(message)}'); try { if (await canLaunchUrl(uri)) { await launchUrl(uri); } else { throw Exception('Не удалось открыть SMS приложение'); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Ошибка отправки: $e')), ); } } } Future _sendNativeShare(String name) async { final message = 'Привет! Присоединяйся к бета-тесту мессенджера Чепухаграм. Напиши разработчику в Telegram @ArturKarasevich для получения доступа!'; try { await Share.share( message, subject: 'Приглашение в Чепухаграм', ); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Ошибка отправки: $e')), ); } } } Future _showInviteDialog(String name, String phone) async { final colorScheme = Theme.of(context).colorScheme; showDialog( context: context, builder: (context) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), title: Text('Пригласить $name', style: const TextStyle(fontWeight: FontWeight.bold)), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Этого человека ещё нет в Чепухаграм. Выберите способ приглашения:', style: TextStyle(color: Colors.grey, fontSize: 14), ), const SizedBox(height: 20), ListTile( contentPadding: EdgeInsets.zero, leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: colorScheme.primary.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon(Icons.sms_rounded, color: colorScheme.primary, size: 20), ), title: const Text('Отправить SMS', style: TextStyle(fontWeight: FontWeight.w600)), subtitle: Text(phone, style: const TextStyle(fontSize: 12)), onTap: () { Navigator.pop(context); _sendInvitation(phone); }, ), const SizedBox(height: 8), ListTile( contentPadding: EdgeInsets.zero, leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: colorScheme.primary.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon(Icons.share_rounded, color: colorScheme.primary, size: 20), ), title: const Text('Другие мессенджеры', style: TextStyle(fontWeight: FontWeight.w600)), subtitle: const Text('Нативное меню выбора (Telegram, WhatsApp и др.)', style: TextStyle(fontSize: 12)), onTap: () { Navigator.pop(context); _sendNativeShare(name); }, ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Отмена'), ), ], ), ); } Widget _buildHeader(String title, ColorScheme colorScheme) { return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), child: Text( title.toUpperCase(), style: TextStyle( fontSize: 11, fontWeight: FontWeight.w700, color: colorScheme.primary, letterSpacing: 1.0, ), ), ); } Widget _buildList(ColorScheme colorScheme) { if (_registeredMatches.isEmpty && _unregisteredMatches.isEmpty) { return const Center(child: Text('Контакты в телефонной книге не найдены.')); } final List listItems = []; if (_registeredMatches.isNotEmpty) { listItems.add(_buildHeader('Контакты в Чепухаграм', colorScheme)); for (int i = 0; i < _registeredMatches.length; i++) { final m = _registeredMatches[i]; final Contact regUser = m['registered']; final fc.Contact devContact = m['device']; final String dispName = devContact.displayName.isNotEmpty ? devContact.displayName : regUser.name; final initials = dispName.isNotEmpty ? dispName .trim() .split(RegExp(r'\s+')) .take(2) .map((e) => e.isNotEmpty ? e[0].toUpperCase() : '') .join() : '?'; listItems.add( ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), leading: Container( width: 40, height: 40, decoration: BoxDecoration( shape: BoxShape.circle, color: colorScheme.primaryContainer, ), child: ClipOval( child: Stack( alignment: Alignment.center, children: [ Text( initials, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: colorScheme.onPrimaryContainer, ), ), if (regUser.effectiveAvatarUrl != null && regUser.effectiveAvatarUrl!.isNotEmpty) CachedNetworkImage( imageUrl: regUser.effectiveAvatarUrl!, fit: BoxFit.cover, width: 40, height: 40, placeholder: (context, url) => const SizedBox.shrink(), errorWidget: (context, url, error) => const SizedBox.shrink(), ), ], ), ), ), title: Text( dispName, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16), ), subtitle: Text( '@${regUser.username} • ${regUser.phone ?? ''}', style: TextStyle(color: colorScheme.onSurface.withOpacity(0.6), fontSize: 13), ), trailing: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.green.withOpacity(0.15), borderRadius: BorderRadius.circular(8), ), child: const Text( 'В Чепухаграм', style: TextStyle(color: Colors.green, fontSize: 11, fontWeight: FontWeight.bold), ), ), onTap: () { // Открываем чат с этим контактом Navigator.pushReplacement( context, MaterialPageRoute( builder: (_) => ChatScreen(contact: regUser), ), ); }, ), ); if (i < _registeredMatches.length - 1) { listItems.add( Divider( height: 1, indent: 72, endIndent: 16, color: colorScheme.outlineVariant.withOpacity(0.15), ), ); } } listItems.add(const SizedBox(height: 16)); } if (_unregisteredMatches.isNotEmpty) { listItems.add(_buildHeader('Пригласить в Чепухаграм', colorScheme)); for (int i = 0; i < _unregisteredMatches.length; i++) { final dc = _unregisteredMatches[i]; final phoneNum = dc.phones.isNotEmpty ? dc.phones.first.number : ''; final initials = dc.displayName.isNotEmpty ? dc.displayName .trim() .split(RegExp(r'\s+')) .take(2) .map((e) => e.isNotEmpty ? e[0].toUpperCase() : '') .join() : '?'; listItems.add( ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), leading: CircleAvatar( backgroundColor: colorScheme.surfaceVariant.withOpacity(0.4), foregroundColor: colorScheme.outline, child: Text( initials, style: const TextStyle(fontWeight: FontWeight.bold), ), ), title: Text( dc.displayName, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16), ), subtitle: Text( phoneNum, style: TextStyle(color: colorScheme.onSurface.withOpacity(0.6), fontSize: 13), ), trailing: OutlinedButton( onPressed: () => _showInviteDialog(dc.displayName, phoneNum), style: OutlinedButton.styleFrom( foregroundColor: colorScheme.primary, side: BorderSide(color: colorScheme.primary.withOpacity(0.5)), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), child: const Text('Пригласить', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)), ), onTap: () => _showInviteDialog(dc.displayName, phoneNum), ), ); if (i < _unregisteredMatches.length - 1) { listItems.add( Divider( height: 1, indent: 72, endIndent: 16, color: colorScheme.outlineVariant.withOpacity(0.15), ), ); } } } return ListView( physics: const BouncingScrollPhysics(), children: listItems, ); } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Scaffold( backgroundColor: colorScheme.background, appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, scrolledUnderElevation: 0, iconTheme: IconThemeData(color: colorScheme.onBackground), title: Text( 'Новый чат', style: TextStyle( color: colorScheme.onBackground, fontWeight: FontWeight.w800, fontSize: 24, letterSpacing: -0.5, ), ), centerTitle: false, ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : _permissionDenied ? Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.contacts_rounded, size: 80, color: colorScheme.outline.withOpacity(0.4), ), const SizedBox(height: 24), const Text( 'Для поиска контактов необходимо разрешение', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), const SizedBox(height: 12), const Text( 'Чепухаграм сопоставит номера из вашей телефонной книги, чтобы вы могли общаться с друзьями.', style: TextStyle(color: Colors.grey, fontSize: 14), textAlign: TextAlign.center, ), const SizedBox(height: 24), ElevatedButton( onPressed: _requestPermissionAndLoad, style: ElevatedButton.styleFrom( backgroundColor: colorScheme.primary, foregroundColor: colorScheme.onPrimary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), child: const Text('Предоставить доступ', style: TextStyle(fontWeight: FontWeight.bold)), ), ], ), ), ) : _error != null ? Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline_rounded, size: 60, color: colorScheme.error), const SizedBox(height: 16), Text('Ошибка: $_error', textAlign: TextAlign.center), const SizedBox(height: 24), ElevatedButton( onPressed: _requestPermissionAndLoad, style: ElevatedButton.styleFrom( backgroundColor: colorScheme.primary, foregroundColor: colorScheme.onPrimary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), child: const Text('Повторить попытку', style: TextStyle(fontWeight: FontWeight.bold)), ), ], ), ), ) : _buildList(colorScheme), ); } }