Chepuhagram/lib/presentation/screens/new_chat_screen.dart

509 lines
19 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: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)),
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),
);
}
}