510 lines
20 KiB
Dart
510 lines
20 KiB
Dart
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<NewChatScreen> createState() => _NewChatScreenState();
|
||
}
|
||
|
||
class _NewChatScreenState extends State<NewChatScreen> {
|
||
bool _isLoading = true;
|
||
bool _permissionDenied = false;
|
||
String? _error;
|
||
|
||
List<Map<String, dynamic>> _registeredMatches = [];
|
||
List<fc.Contact> _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<void> _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<String, Contact> registeredMap = {};
|
||
for (var u in registeredUsers) {
|
||
if (u.phone != null) {
|
||
final norm = _normalizePhone(u.phone!);
|
||
if (norm.isNotEmpty) {
|
||
registeredMap[norm] = u;
|
||
}
|
||
}
|
||
}
|
||
|
||
final List<Map<String, dynamic>> regMatches = [];
|
||
final List<fc.Contact> 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<void> _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<void> _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<void> _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<Widget> 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),
|
||
);
|
||
}
|
||
} |