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