22-06-2026+10-13
This commit is contained in:
parent
c3999db9eb
commit
04c3772bb6
|
|
@ -1,5 +1,6 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class Contact {
|
|||
final int? lastMessageId;
|
||||
final MessageType? lastMessageType;
|
||||
int? firstUnreadMessageId;
|
||||
final String? phone;
|
||||
|
||||
String? get effectiveAvatarUrl {
|
||||
if (avatarFileId != null && avatarFileId!.isNotEmpty) {
|
||||
|
|
@ -42,6 +43,7 @@ class Contact {
|
|||
this.lastMessageId,
|
||||
this.lastMessageType,
|
||||
this.firstUnreadMessageId,
|
||||
this.phone,
|
||||
});
|
||||
|
||||
Contact copyWith({
|
||||
|
|
@ -60,6 +62,7 @@ class Contact {
|
|||
int? lastMessageId,
|
||||
MessageType? lastMessageType,
|
||||
int? firstUnreadMessageId,
|
||||
String? phone,
|
||||
}) {
|
||||
return Contact(
|
||||
id: id ?? this.id,
|
||||
|
|
@ -77,6 +80,7 @@ class Contact {
|
|||
lastMessageId: lastMessageId ?? this.lastMessageId,
|
||||
lastMessageType: lastMessageType ?? this.lastMessageType,
|
||||
firstUnreadMessageId: firstUnreadMessageId ?? this.firstUnreadMessageId,
|
||||
phone: phone ?? this.phone,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -104,6 +108,7 @@ class Contact {
|
|||
lastMessageId: int.tryParse((json['last_message_id'] ?? json['lastMessageId'] ?? 0).toString()) ?? 0,
|
||||
lastMessageType: MessageModel.parseMessageType(json['last_message_type'] ?? json['lastMessageType'] ?? 'text'),
|
||||
firstUnreadMessageId: int.tryParse((json['first_unread_message_id'] ?? json['firstUnreadMessageId'] ?? 0).toString()) ?? 0,
|
||||
phone: json['phone'] ?? json['phone_number'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -451,8 +451,12 @@ class AuthProvider extends ChangeNotifier {
|
|||
Future<bool> setupAccount(
|
||||
String firstName,
|
||||
String? lastName,
|
||||
String masterPassword,
|
||||
) async {
|
||||
String masterPassword, {
|
||||
String? phone,
|
||||
String? email,
|
||||
String? about,
|
||||
}) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
|
|
@ -476,7 +480,18 @@ class AuthProvider extends ChangeNotifier {
|
|||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final String currentUsername = _username ?? '';
|
||||
await _apiService.updateMe(
|
||||
username: currentUsername,
|
||||
firstName: firstName,
|
||||
lastName: lastName ?? '',
|
||||
phone: phone,
|
||||
email: email,
|
||||
about: about,
|
||||
);
|
||||
|
||||
_needsSetup = false;
|
||||
await _checkAccountStatus();
|
||||
notifyListeners();
|
||||
return true;
|
||||
} else {
|
||||
|
|
@ -487,6 +502,7 @@ class AuthProvider extends ChangeNotifier {
|
|||
print("Ошибка сети: $e");
|
||||
return false;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -243,6 +243,13 @@ class ContactProvider extends ChangeNotifier {
|
|||
try {
|
||||
final index = _contacts.indexWhere((c) => c.id == contactId);
|
||||
if (index != -1) {
|
||||
if (lastMessage == null && lastMessageTime == null && lastMessageId == null) {
|
||||
_contacts.removeAt(index);
|
||||
print("Контакт $contactId удален из списка чатов, так как сообщений не осталось.");
|
||||
_sortContacts();
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
final existing = _contacts[index];
|
||||
String displayMessage;
|
||||
if (isEdited) {
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ class _AccountSettingsScreenState extends State<AccountSettingsScreen> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
Future<bool> _save() async {
|
||||
if (!_formKey.currentState!.validate()) return false;
|
||||
setState(() => _isSaving = true);
|
||||
try {
|
||||
final api = ApiService();
|
||||
|
|
@ -57,19 +57,16 @@ class _AccountSettingsScreenState extends State<AccountSettingsScreen> {
|
|||
about: _aboutController.text,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
if (!mounted) return true;
|
||||
await context.read<AuthProvider>().refreshMe();
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Сохранено'), behavior: SnackBarBehavior.floating),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(e.toString().replaceAll('Exception: ', '')), behavior: SnackBarBehavior.floating),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
if (mounted) setState(() => _isSaving = false);
|
||||
}
|
||||
|
|
@ -79,31 +76,18 @@ class _AccountSettingsScreenState extends State<AccountSettingsScreen> {
|
|||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
final success = await _save();
|
||||
return success;
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: colorScheme.background,
|
||||
appBar: AppBar(
|
||||
title: Text('Редактировать аккаунт', style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface)),
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
iconTheme: IconThemeData(color: Theme.of(context).colorScheme.onSurface),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Center(
|
||||
child: _isSaving
|
||||
? SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2.5, color: colorScheme.primary),
|
||||
)
|
||||
: TextButton.icon(
|
||||
onPressed: _save,
|
||||
icon: const Icon(Icons.done_rounded, size: 18),
|
||||
label: const Text('Готово', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
|
|
@ -159,20 +143,23 @@ class _AccountSettingsScreenState extends State<AccountSettingsScreen> {
|
|||
label: 'О себе',
|
||||
hint: 'Расскажите немного о себе',
|
||||
icon: Icons.short_text_rounded,
|
||||
maxLines: 4,
|
||||
minLines: 1,
|
||||
maxLines: 50,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildInputField({
|
||||
required TextEditingController controller,
|
||||
required String label,
|
||||
required String hint,
|
||||
required IconData icon,
|
||||
int maxLines = 1,
|
||||
int? minLines,
|
||||
int? maxLines = 1,
|
||||
TextInputType? keyboardType,
|
||||
String? Function(String?)? validator,
|
||||
}) {
|
||||
|
|
@ -188,6 +175,7 @@ class _AccountSettingsScreenState extends State<AccountSettingsScreen> {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
minLines: minLines,
|
||||
maxLines: maxLines,
|
||||
keyboardType: keyboardType,
|
||||
validator: validator,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ class _AccountSetupScreenState extends State<AccountSetupScreen>
|
|||
final _formKey = GlobalKey<FormState>();
|
||||
final _firstNameController = TextEditingController();
|
||||
final _lastNameController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _aboutController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _confirmPasswordController = TextEditingController();
|
||||
|
||||
|
|
@ -46,6 +49,14 @@ class _AccountSetupScreenState extends State<AccountSetupScreen>
|
|||
);
|
||||
|
||||
_animationController.forward();
|
||||
|
||||
// Заполняем поля данными, если они уже есть на сервере
|
||||
final auth = context.read<AuthProvider>();
|
||||
_firstNameController.text = auth.firstName ?? '';
|
||||
_lastNameController.text = auth.lastName ?? '';
|
||||
_phoneController.text = auth.phone ?? '';
|
||||
_emailController.text = auth.email ?? '';
|
||||
_aboutController.text = auth.about ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -91,7 +102,7 @@ class _AccountSetupScreenState extends State<AccountSetupScreen>
|
|||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
"Укажите ваше имя и создайте мастер-пароль",
|
||||
"Укажите ваши данные и создайте мастер-пароль",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
|
|
@ -102,7 +113,7 @@ class _AccountSetupScreenState extends State<AccountSetupScreen>
|
|||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
const SizedBox(height: 36),
|
||||
|
||||
// Поле Имя
|
||||
_buildTextField(
|
||||
|
|
@ -120,6 +131,33 @@ class _AccountSetupScreenState extends State<AccountSetupScreen>
|
|||
label: "Фамилия",
|
||||
icon: Icons.person_outline,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Поле Телефон
|
||||
_buildTextField(
|
||||
controller: _phoneController,
|
||||
label: "Телефон",
|
||||
icon: Icons.phone_android_outlined,
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Поле Почта
|
||||
_buildTextField(
|
||||
controller: _emailController,
|
||||
label: "Электронная почта",
|
||||
icon: Icons.mail_outline_rounded,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Поле О себе
|
||||
_buildTextField(
|
||||
controller: _aboutController,
|
||||
label: "О себе",
|
||||
icon: Icons.info_outline_rounded,
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Поле Мастер-пароль
|
||||
|
|
@ -153,7 +191,7 @@ class _AccountSetupScreenState extends State<AccountSetupScreen>
|
|||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Кнопка Продолжить
|
||||
ElevatedButton(
|
||||
|
|
@ -201,6 +239,8 @@ class _AccountSetupScreenState extends State<AccountSetupScreen>
|
|||
required IconData icon,
|
||||
String? Function(String?)? validator,
|
||||
bool obscureText = false,
|
||||
TextInputType? keyboardType,
|
||||
int maxLines = 1,
|
||||
}) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
|
|
@ -221,6 +261,8 @@ class _AccountSetupScreenState extends State<AccountSetupScreen>
|
|||
controller: controller,
|
||||
validator: validator,
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
maxLines: maxLines,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
labelStyle: TextStyle(color: colorScheme.outline),
|
||||
|
|
@ -247,6 +289,9 @@ class _AccountSetupScreenState extends State<AccountSetupScreen>
|
|||
_firstNameController.text.trim(),
|
||||
_lastNameController.text.trim(),
|
||||
_passwordController.text,
|
||||
phone: _phoneController.text.trim(),
|
||||
email: _emailController.text.trim(),
|
||||
about: _aboutController.text.trim(),
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
|
|
@ -272,6 +317,9 @@ class _AccountSetupScreenState extends State<AccountSetupScreen>
|
|||
void dispose() {
|
||||
_firstNameController.dispose();
|
||||
_lastNameController.dispose();
|
||||
_phoneController.dispose();
|
||||
_emailController.dispose();
|
||||
_aboutController.dispose();
|
||||
_passwordController.dispose();
|
||||
_confirmPasswordController.dispose();
|
||||
_animationController.dispose();
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import 'package:drift/drift.dart' as drift;
|
|||
import 'dart:math' as math;
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart' as fc;
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
final Contact contact;
|
||||
|
|
@ -230,14 +231,48 @@ class _ChatScreenState extends State<ChatScreen> with RouteAware {
|
|||
final String? savedSurname = prefs.getString(
|
||||
'lastname_${_currentContact.id}',
|
||||
);
|
||||
print('Загружены имя $savedName, $savedSurname');
|
||||
|
||||
String? phoneBookName;
|
||||
if ((savedName == null && savedSurname == null) &&
|
||||
_currentContact.phone != null &&
|
||||
(Platform.isAndroid || Platform.isIOS)) {
|
||||
try {
|
||||
final hasPermission = await fc.FlutterContacts.requestPermission(readonly: true);
|
||||
if (hasPermission) {
|
||||
final deviceContacts = await fc.FlutterContacts.getContacts(withProperties: true);
|
||||
String normalizePhone(String p) {
|
||||
final digits = p.replaceAll(RegExp(r'\D'), '');
|
||||
if (digits.length >= 10) {
|
||||
return digits.substring(digits.length - 10);
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
final normTarget = normalizePhone(_currentContact.phone!);
|
||||
if (normTarget.isNotEmpty) {
|
||||
for (var dc in deviceContacts) {
|
||||
for (var phoneObj in dc.phones) {
|
||||
if (normalizePhone(phoneObj.number) == normTarget) {
|
||||
phoneBookName = dc.displayName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (phoneBookName != null) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print("Ошибка получения имени контакта из телефонной книги в чате: $e");
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
if (savedName != null) {
|
||||
_currentContact.name = savedName;
|
||||
}
|
||||
if (savedSurname != null) {
|
||||
_currentContact.surname = savedSurname;
|
||||
if (savedName != null || savedSurname != null) {
|
||||
_currentContact.name = savedName ?? '';
|
||||
_currentContact.surname = savedSurname ?? '';
|
||||
} else if (phoneBookName != null && phoneBookName!.isNotEmpty) {
|
||||
_currentContact.name = phoneBookName!;
|
||||
_currentContact.surname = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import 'admin_broadcast_screen.dart';
|
|||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart' as fc;
|
||||
|
||||
class ContactsScreen extends StatefulWidget {
|
||||
final int? targetChatId;
|
||||
|
|
@ -71,6 +72,7 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
|
|||
|
||||
// Хранилище стабильно загруженных локальных имён
|
||||
Map<int, String> _localFullNames = {};
|
||||
Map<int, String> _phoneBookNames = {};
|
||||
|
||||
final Map<int, int> _pendingUnreadCounters = {};
|
||||
|
||||
|
|
@ -128,11 +130,64 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
|
|||
});
|
||||
}
|
||||
|
||||
Future<void> _loadPhoneBookNames() async {
|
||||
if (!Platform.isAndroid && !Platform.isIOS) return;
|
||||
try {
|
||||
final hasPermission = await fc.FlutterContacts.requestPermission(readonly: true);
|
||||
if (!hasPermission) return;
|
||||
|
||||
final deviceContacts = await fc.FlutterContacts.getContacts(withProperties: true);
|
||||
final contactProvider = context.read<ContactProvider>();
|
||||
|
||||
final Map<int, String> matchedNames = {};
|
||||
|
||||
String normalizePhone(String p) {
|
||||
final digits = p.replaceAll(RegExp(r'\D'), '');
|
||||
if (digits.length >= 10) {
|
||||
return digits.substring(digits.length - 10);
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
|
||||
final Map<String, int> phoneToContactId = {};
|
||||
for (var contact in contactProvider.contacts) {
|
||||
if (contact.phone != null) {
|
||||
final norm = normalizePhone(contact.phone!);
|
||||
if (norm.isNotEmpty) {
|
||||
phoneToContactId[norm] = contact.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var dc in deviceContacts) {
|
||||
for (var phoneObj in dc.phones) {
|
||||
final norm = normalizePhone(phoneObj.number);
|
||||
if (norm.isNotEmpty && phoneToContactId.containsKey(norm)) {
|
||||
final contactId = phoneToContactId[norm]!;
|
||||
if (dc.displayName.isNotEmpty) {
|
||||
matchedNames[contactId] = dc.displayName;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_phoneBookNames = matchedNames;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
print("Ошибка загрузки имен из телефонной книги: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initContacts() async {
|
||||
if (_contactsLoaded) return;
|
||||
final contactProvider = context.read<ContactProvider>();
|
||||
await contactProvider.loadContacts();
|
||||
await _loadLocalNames(); // Гарантированный вызов после загрузки контактов
|
||||
await _loadLocalNames();
|
||||
await _loadPhoneBookNames();
|
||||
|
||||
print('Contacts loaded, checking targetChatId: ${widget.targetChatId}');
|
||||
|
||||
|
|
@ -404,8 +459,11 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
|
|||
|
||||
Widget _buildSearchResultTile(Contact contact, {required bool isChat}) {
|
||||
final localName = _localFullNames[contact.id];
|
||||
final phoneBookName = _phoneBookNames[contact.id];
|
||||
final displayName = (localName != null && localName.isNotEmpty)
|
||||
? localName
|
||||
: (phoneBookName != null && phoneBookName.isNotEmpty)
|
||||
? phoneBookName
|
||||
: '${contact.name != 'Unknown' ? contact.name : ''} ${contact.surname != 'Unknown' ? contact.surname : ''}'
|
||||
.trim();
|
||||
final title = displayName.isNotEmpty ? displayName : contact.username;
|
||||
|
|
@ -560,8 +618,11 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
|
|||
final isSelected = _selectedContact?.id == contact.id;
|
||||
|
||||
final localName = _localFullNames[contact.id];
|
||||
final phoneBookName = _phoneBookNames[contact.id];
|
||||
final displayName = (localName != null && localName.isNotEmpty)
|
||||
? localName
|
||||
: (phoneBookName != null && phoneBookName.isNotEmpty)
|
||||
? phoneBookName
|
||||
: '${contact.name != 'Unknown' ? contact.name : ''} ${contact.surname != 'Unknown' ? contact.surname : ''}'
|
||||
.trim();
|
||||
|
||||
|
|
@ -952,10 +1013,8 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
|
|||
SafeArea(top: false, child: _buildUpdateBanner(isPhone)),
|
||||
],
|
||||
),
|
||||
floatingActionButton: null,
|
||||
/* (isCollapsed || (isPhone && _currentIndex != 0))
|
||||
? null
|
||||
: AnimatedPadding(
|
||||
floatingActionButton: (Platform.isAndroid && isPhone && _currentIndex == 0)
|
||||
? AnimatedPadding(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.easeInOut,
|
||||
padding: EdgeInsets.only(
|
||||
|
|
@ -972,7 +1031,8 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
|
|||
),
|
||||
child: const Icon(Icons.edit_note_rounded),
|
||||
),
|
||||
),*/
|
||||
)
|
||||
: null,
|
||||
bottomNavigationBar: isPhone
|
||||
? BottomNavigationBar(
|
||||
currentIndex: _currentIndex,
|
||||
|
|
@ -1825,6 +1885,20 @@ class _ContactsScreenState extends State<ContactsScreen> with RouteAware {
|
|||
}
|
||||
}
|
||||
|
||||
if (data['type'] == 'message_deleted') {
|
||||
final messageId = int.tryParse(data['message_id']?.toString() ?? '');
|
||||
if (messageId != null) {
|
||||
final contactIndex = contactProvider.contacts.indexWhere(
|
||||
(c) => c.lastMessageId == messageId,
|
||||
);
|
||||
if (contactIndex != -1) {
|
||||
final contactId = contactProvider.contacts[contactIndex].id;
|
||||
await contactProvider.refreshContactLastMessage(contactId);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (data['type'] == 'user_updated') {
|
||||
final userId = int.tryParse(data['user_id']?.toString() ?? '');
|
||||
if (userId != null) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
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 '/logic/contact_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});
|
||||
|
|
@ -12,52 +18,493 @@ class NewChatScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
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();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final authProvider = context.read<AuthProvider>();
|
||||
final contactProvider = context.read<ContactProvider>();
|
||||
_requestPermissionAndLoad();
|
||||
}
|
||||
|
||||
// Установить текущего пользователя и загрузить все контакты
|
||||
contactProvider.setCurrentUserId(authProvider.currentUserId);
|
||||
contactProvider.loadAllContactsForNewChat();
|
||||
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 contactProvider = context.watch<ContactProvider>();
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: colorScheme.background,
|
||||
appBar: AppBar(
|
||||
title: const Text('Новый чат'),
|
||||
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,
|
||||
),
|
||||
body: contactProvider.isLoading
|
||||
),
|
||||
centerTitle: false,
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: contactProvider.error != null
|
||||
? Center(child: Text('Error: ${contactProvider.error}'))
|
||||
: ListView.builder(
|
||||
itemCount: contactProvider.allContacts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final contact = contactProvider.allContacts[index];
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
child: Text(contact.name[0]),
|
||||
: _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),
|
||||
),
|
||||
title: Text(contact.name),
|
||||
onTap: () {
|
||||
// Создать чат с этим контактом
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ChatScreen(contact: contact),
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -87,12 +87,6 @@ class _PrivacySettingsScreenState extends State<PrivacySettingsScreen> {
|
|||
await _savePreference(_showAvatarKey, _showAvatar);
|
||||
await _savePreference(_showAboutKey, _showAbout);
|
||||
await _savePreference(_showLastOnlineKey, _showLastOnline);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Настройки видимости сохранены'), behavior: SnackBarBehavior.floating),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
|
|
@ -109,27 +103,18 @@ class _PrivacySettingsScreenState extends State<PrivacySettingsScreen> {
|
|||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
await _saveToServer();
|
||||
return true;
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: colorScheme.background,
|
||||
appBar: AppBar(
|
||||
title: Text('Видимость данных', style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSurface)),
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
iconTheme: IconThemeData(color: Theme.of(context).colorScheme.onSurface),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Center(
|
||||
child: _isSaving
|
||||
? SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: colorScheme.primary))
|
||||
: TextButton.icon(
|
||||
onPressed: _saveToServer,
|
||||
icon: const Icon(Icons.save_rounded, size: 18),
|
||||
label: const Text('Сохранить'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
|
|
@ -182,8 +167,9 @@ class _PrivacySettingsScreenState extends State<PrivacySettingsScreen> {
|
|||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSwitchTile(String title, bool value, ValueChanged<bool> onChanged) {
|
||||
return SwitchListTile(
|
||||
|
|
|
|||
|
|
@ -556,7 +556,7 @@ class _SecuritySettingsScreenState extends State<SecuritySettingsScreen> {
|
|||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Обновить ключ шифрования'),
|
||||
: const Text('Обновить пароль шифрования'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
|
|||
import '/core/constants.dart';
|
||||
import 'dart:io';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart' as fc;
|
||||
|
||||
class UserProfileScreen extends StatefulWidget {
|
||||
final int userId;
|
||||
|
|
@ -35,6 +36,7 @@ class _UserProfileScreenState extends State<UserProfileScreen> {
|
|||
Timer? _onlineTimer;
|
||||
String? firstName;
|
||||
String? lastName;
|
||||
String? _phoneBookName;
|
||||
bool _isAvatarExpanded = false;
|
||||
|
||||
@override
|
||||
|
|
@ -62,9 +64,45 @@ class _UserProfileScreenState extends State<UserProfileScreen> {
|
|||
final prefs = await SharedPreferences.getInstance();
|
||||
firstName = prefs.getString('firstname_${widget.userId}');
|
||||
lastName = prefs.getString('lastname_${widget.userId}');
|
||||
|
||||
String? phoneBookName;
|
||||
final phone = data['phone']?.toString();
|
||||
if ((firstName == null && lastName == null) &&
|
||||
phone != null &&
|
||||
(Platform.isAndroid || Platform.isIOS)) {
|
||||
try {
|
||||
final hasPermission = await fc.FlutterContacts.requestPermission(readonly: true);
|
||||
if (hasPermission) {
|
||||
final deviceContacts = await fc.FlutterContacts.getContacts(withProperties: true);
|
||||
String normalizePhone(String p) {
|
||||
final digits = p.replaceAll(RegExp(r'\D'), '');
|
||||
if (digits.length >= 10) {
|
||||
return digits.substring(digits.length - 10);
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
final normTarget = normalizePhone(phone);
|
||||
if (normTarget.isNotEmpty) {
|
||||
for (var dc in deviceContacts) {
|
||||
for (var phoneObj in dc.phones) {
|
||||
if (normalizePhone(phoneObj.number) == normTarget) {
|
||||
phoneBookName = dc.displayName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (phoneBookName != null) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print("Ошибка получения имени контакта из телефонной книги в профиле: $e");
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_userData = data;
|
||||
_phoneBookName = phoneBookName;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
|
@ -211,8 +249,10 @@ class _UserProfileScreenState extends State<UserProfileScreen> {
|
|||
if (_userData == null) return const SizedBox.shrink();
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final String displayFN = firstName ?? _userData?['first_name'] ?? '';
|
||||
final String displayLN = lastName ?? _userData?['last_name'] ?? '';
|
||||
final String displayFN = firstName ?? _phoneBookName ?? _userData?['first_name'] ?? '';
|
||||
final String displayLN = (firstName == null && _phoneBookName != null)
|
||||
? ''
|
||||
: (lastName ?? _userData?['last_name'] ?? '');
|
||||
final String combinedName = '$displayFN $displayLN'.trim();
|
||||
final String username = _userData?['username'] ?? '';
|
||||
final rawAvatarUrl = _userData?['avatar_url']?.toString();
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||
Future<void>? _delayFuture;
|
||||
|
||||
TextSelection? _currentSelection;
|
||||
TapDownDetails? _lastTapDownDetails;
|
||||
|
||||
final MediaCacheManager _mediaCache = MediaCacheManager();
|
||||
|
||||
|
|
@ -737,7 +738,16 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: widget.onTap,
|
||||
onTapDown: (details) {
|
||||
_lastTapDownDetails = details;
|
||||
},
|
||||
onTap: () {
|
||||
if (_lastTapDownDetails != null) {
|
||||
_showContextMenu(_lastTapDownDetails!);
|
||||
} else {
|
||||
widget.onTap?.call();
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: const Radius.circular(16),
|
||||
topRight: const Radius.circular(16),
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import package_info_plus
|
|||
import path_provider_foundation
|
||||
import photo_manager
|
||||
import record_macos
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import sqflite_darwin
|
||||
import sqlite3_flutter_libs
|
||||
|
|
@ -53,6 +54,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "PhotoManagerPlugin"))
|
||||
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
|
||||
|
|
|
|||
28
pubspec.lock
28
pubspec.lock
|
|
@ -638,6 +638,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
flutter_contacts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_contacts
|
||||
sha256: "388d32cd33f16640ee169570128c933b45f3259bddbfae7a100bb49e5ffea9ae"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.9+2"
|
||||
flutter_image_compress:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1164,10 +1172,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "1.0.6"
|
||||
mobile_scanner:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1520,6 +1528,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.8"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.2"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ dependencies:
|
|||
scrollable_positioned_list: ^0.3.8
|
||||
qr_flutter: ^4.1.0
|
||||
mobile_scanner: ^7.2.0
|
||||
flutter_contacts: ^1.1.6
|
||||
share_plus: ^7.2.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ async def get_privacy_settings(current_user: models.User = Depends(get_current_u
|
|||
|
||||
@usersRouter.get("/all")
|
||||
async def read_users_all(
|
||||
request: Request,
|
||||
current_user: models.User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
query: Optional[str] = None,
|
||||
|
|
@ -233,7 +234,18 @@ async def read_users_all(
|
|||
users_for_return.append(user)
|
||||
else:
|
||||
users_for_return = users
|
||||
return [{"id": user.id, "username": user.username, "name": f"{user.first_name} {user.last_name or ''}".strip(), "public_key": user.public_key} for user in users_for_return]
|
||||
return [
|
||||
{
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"name": f"{user.first_name} {user.last_name or ''}".strip(),
|
||||
"public_key": user.public_key,
|
||||
"phone": user.phone,
|
||||
"avatar_file_id": user.avatar_file_id if (user.show_avatar or current_user.id == 1) else None,
|
||||
"avatar_url": str(request.url_for("get_file", file_id=user.avatar_file_id)) if (user.show_avatar or current_user.id == 1) and user.avatar_file_id else None,
|
||||
}
|
||||
for user in users_for_return
|
||||
]
|
||||
|
||||
@usersRouter.get("/chats")
|
||||
async def read_users_chats(
|
||||
|
|
@ -296,6 +308,7 @@ async def read_users_chats(
|
|||
"online": str(user.id) in connection_manager.manager.online_users,
|
||||
"last_message_id": last_msg.id if last_msg else None,
|
||||
"last_message_type": last_msg.message_type if last_msg else None,
|
||||
"phone": user.phone,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
#include <local_auth_windows/local_auth_plugin.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <record_windows/record_windows_plugin_c_api.h>
|
||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
#include <video_player_win/video_player_win_plugin_c_api.h>
|
||||
|
|
@ -41,6 +42,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
RecordWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||
local_auth_windows
|
||||
permission_handler_windows
|
||||
record_windows
|
||||
share_plus
|
||||
sqlite3_flutter_libs
|
||||
url_launcher_windows
|
||||
video_player_win
|
||||
|
|
|
|||
Loading…
Reference in New Issue