feat(contacts): имена из телефонной книги везде + тумблер в дев-меню
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../backend/modules/contacts.dart';
|
||||
import '../../core/config/app_commands.dart';
|
||||
import '../../core/config/app_digital_id_mode.dart';
|
||||
import '../../core/config/app_link_preview.dart';
|
||||
import '../../core/config/app_phonebook_names.dart';
|
||||
import '../../core/config/app_pranks.dart';
|
||||
import '../../core/config/app_show_extra_info.dart';
|
||||
import '../../core/config/app_stories.dart';
|
||||
import '../../core/config/app_swipe_back_desktop.dart';
|
||||
import '../../core/contacts/device_contacts_service.dart';
|
||||
import '../screens/digital_id/digital_id_web_screen.dart';
|
||||
import '../widgets/custom_notification.dart';
|
||||
import 'debug_toggle_tile.dart';
|
||||
@@ -15,6 +18,23 @@ import 'debug_toggle_tile.dart';
|
||||
class DebugFeatureTogglesSection extends StatelessWidget {
|
||||
const DebugFeatureTogglesSection({super.key});
|
||||
|
||||
Future<void> _onPhonebookNamesChanged(
|
||||
BuildContext context,
|
||||
bool value,
|
||||
) async {
|
||||
await AppPhonebookNames.save(value);
|
||||
if (value) {
|
||||
final ok = await DeviceContactsService.reload();
|
||||
if (!ok && context.mounted) {
|
||||
showCustomNotification(
|
||||
context,
|
||||
'Не удалось загрузить контакты телефона',
|
||||
);
|
||||
}
|
||||
}
|
||||
ContactsModule.revision.value++;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
@@ -122,6 +142,19 @@ class DebugFeatureTogglesSection extends StatelessWidget {
|
||||
onChanged: AppStories.save,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: DebugToggleTile(
|
||||
icon: Symbols.contacts,
|
||||
title: 'Имена из телефонной книги',
|
||||
subtitle: (v) => v
|
||||
? 'Имена собеседников показываются так, как записаны в '
|
||||
'телефонной книге устройства'
|
||||
: 'Имена показываются так, как их прислал сервер',
|
||||
valueListenable: AppPhonebookNames.current,
|
||||
onChanged: (v) => _onPhonebookNamesChanged(context, v),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: DebugToggleTile(
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../backend/modules/chats.dart';
|
||||
import '../../../backend/modules/contacts.dart';
|
||||
import '../../../backend/modules/messages.dart' show ContactCache;
|
||||
import '../../../core/storage/app_database.dart';
|
||||
import '../../../core/utils/debouncer.dart';
|
||||
import '../../../core/utils/names.dart';
|
||||
@@ -128,6 +129,11 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
}
|
||||
|
||||
String _contactName(Map<String, dynamic> row) {
|
||||
final id = row['id'];
|
||||
if (id is int) {
|
||||
final cached = ContactCache.get(id);
|
||||
if (cached != null && cached.isNotEmpty) return cached;
|
||||
}
|
||||
return displayName(
|
||||
row['first_name'],
|
||||
row['last_name'],
|
||||
@@ -135,6 +141,10 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
String _phoneResultName(PhoneLookupResult result) {
|
||||
return ContactCache.get(result.id) ?? result.name ?? 'User #${result.id}';
|
||||
}
|
||||
|
||||
void _openChat(int chatId, String name, String? avatarUrl, String type) {
|
||||
pushSwipeable(
|
||||
context,
|
||||
@@ -170,7 +180,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
openContactDialogProfile(
|
||||
context,
|
||||
contactId: result.id,
|
||||
name: result.name ?? 'User #${result.id}',
|
||||
name: _phoneResultName(result),
|
||||
avatarUrl: result.avatarUrl,
|
||||
),
|
||||
);
|
||||
@@ -245,7 +255,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
if (phoneResult != null) ...[
|
||||
_sectionHeader(cs, 'По номеру'),
|
||||
_ResultTile(
|
||||
name: phoneResult.name ?? '',
|
||||
name: _phoneResultName(phoneResult),
|
||||
imageUrl: phoneResult.avatarUrl,
|
||||
subtitle: query,
|
||||
onTap: () => _openPhoneResult(phoneResult),
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../../core/config/debug_test.dart';
|
||||
import '../../../core/contacts/device_contacts_service.dart';
|
||||
import '../../../core/protocol/opcode_map.dart';
|
||||
import '../../../core/protocol/packet.dart';
|
||||
import '../../../core/storage/app_database.dart';
|
||||
import '../../../core/storage/token_storage.dart';
|
||||
import '../../../backend/modules/contacts.dart';
|
||||
import '../../../backend/modules/messages.dart' show ContactCache;
|
||||
import '../../../main.dart';
|
||||
import '../../../models/contact_info.dart';
|
||||
import '../../widgets/komet_avatar.dart';
|
||||
@@ -17,6 +19,8 @@ import '../chats/chat_info_screen.dart';
|
||||
import 'nfc_exchange_sheet.dart';
|
||||
import 'open_contact_profile.dart';
|
||||
|
||||
enum _SearchMode { phone, id }
|
||||
|
||||
class ContactsTab extends StatefulWidget {
|
||||
const ContactsTab({super.key});
|
||||
|
||||
@@ -33,6 +37,12 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
super.initState();
|
||||
_loadContacts();
|
||||
ContactsModule.revision.addListener(_loadContacts);
|
||||
_loadDeviceContacts();
|
||||
}
|
||||
|
||||
Future<void> _loadDeviceContacts() async {
|
||||
final changed = await DeviceContactsService.ensureLoadedInteractive();
|
||||
if (changed && mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -115,7 +125,9 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
final fullName =
|
||||
'${contact.firstName}${contact.lastName != null ? ' ${contact.lastName}' : ''}'
|
||||
.trim();
|
||||
final nameToDisplay = fullName.isEmpty ? '+${contact.phone}' : fullName;
|
||||
final book = DeviceContactsService.nameForPhone(contact.phone);
|
||||
final nameToDisplay =
|
||||
book ?? (fullName.isEmpty ? '+${contact.phone}' : fullName);
|
||||
|
||||
return SpringyTap(
|
||||
child: Material(
|
||||
@@ -284,6 +296,7 @@ class _SearchContactSheet extends StatefulWidget {
|
||||
|
||||
class _SearchContactSheetState extends State<_SearchContactSheet> {
|
||||
final _controller = TextEditingController();
|
||||
_SearchMode _mode = _SearchMode.phone;
|
||||
bool _loading = false;
|
||||
String? _error;
|
||||
|
||||
@@ -293,7 +306,82 @@ class _SearchContactSheetState extends State<_SearchContactSheet> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _setMode(_SearchMode mode) {
|
||||
if (_mode == mode || _loading) return;
|
||||
setState(() {
|
||||
_mode = mode;
|
||||
_error = null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (_mode == _SearchMode.phone) {
|
||||
await _submitPhone();
|
||||
} else {
|
||||
await _submitId();
|
||||
}
|
||||
}
|
||||
|
||||
String? _phoneCandidate(String query) {
|
||||
if (!RegExp(r'^[+\d\s\-()]+$').hasMatch(query)) return null;
|
||||
final digits = query.replaceAll(RegExp(r'[^\d]'), '');
|
||||
if (digits.length < 5) return null;
|
||||
return query;
|
||||
}
|
||||
|
||||
Future<void> _submitPhone() async {
|
||||
final query = _phoneCandidate(_controller.text.trim());
|
||||
if (query == null) {
|
||||
setState(() => _error = 'Введите корректный номер телефона');
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final result = await ContactsModule.findByPhone(api, query);
|
||||
if (!mounted) return;
|
||||
if (result == null) {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
_error = 'Контакт с таким номером не найден';
|
||||
});
|
||||
return;
|
||||
}
|
||||
final navigator = Navigator.of(context);
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
final existing = accountId == null
|
||||
? null
|
||||
: await AppDatabase.findDialogChatByParticipant(accountId, result.id);
|
||||
final chatId = existing ?? ((accountId ?? 0) ^ result.id);
|
||||
if (!mounted) return;
|
||||
navigator.pop();
|
||||
navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ChatInfoScreen(
|
||||
chatId: chatId,
|
||||
name:
|
||||
ContactCache.get(result.id) ??
|
||||
result.name ??
|
||||
'User #${result.id}',
|
||||
imageUrl: result.avatarUrl ?? '',
|
||||
chatType: 'DIALOG',
|
||||
dialogPeerId: result.id,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
_error = 'Ошибка: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _submitId() async {
|
||||
final raw = _controller.text.trim();
|
||||
final id = int.tryParse(raw);
|
||||
if (id == null) {
|
||||
@@ -320,6 +408,7 @@ class _SearchContactSheetState extends State<_SearchContactSheet> {
|
||||
}
|
||||
final raw = Map<String, dynamic>.from(contacts.first as Map);
|
||||
final info = ContactInfo.fromMap(raw);
|
||||
ContactsModule.primeContactCache(raw);
|
||||
if (!mounted) return;
|
||||
final navigator = Navigator.of(context);
|
||||
final accountId = await TokenStorage.getActiveAccountId();
|
||||
@@ -333,7 +422,7 @@ class _SearchContactSheetState extends State<_SearchContactSheet> {
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ChatInfoScreen(
|
||||
chatId: chatId,
|
||||
name: info.displayName ?? 'User #$id',
|
||||
name: ContactCache.get(id) ?? info.displayName ?? 'User #$id',
|
||||
imageUrl: info.avatarUrl ?? '',
|
||||
chatType: 'DIALOG',
|
||||
dialogPeerId: id,
|
||||
@@ -374,7 +463,7 @@ class _SearchContactSheetState extends State<_SearchContactSheet> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Поиск по ID',
|
||||
'Найти контакт',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 18,
|
||||
@@ -388,11 +477,34 @@ class _SearchContactSheetState extends State<_SearchContactSheet> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 12),
|
||||
SegmentedButton<_SearchMode>(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: _SearchMode.phone,
|
||||
label: Text('Номер'),
|
||||
icon: Icon(Symbols.call, size: 18),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: _SearchMode.id,
|
||||
label: Text('ID'),
|
||||
icon: Icon(Symbols.tag, size: 18),
|
||||
),
|
||||
],
|
||||
selected: {_mode},
|
||||
onSelectionChanged: (s) => _setMode(s.first),
|
||||
showSelectedIcon: false,
|
||||
style: ButtonStyle(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _controller,
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.number,
|
||||
keyboardType: _mode == _SearchMode.phone
|
||||
? TextInputType.phone
|
||||
: TextInputType.number,
|
||||
enabled: !_loading,
|
||||
onSubmitted: (_) => _submit(),
|
||||
onChanged: (_) {
|
||||
@@ -400,13 +512,15 @@ class _SearchContactSheetState extends State<_SearchContactSheet> {
|
||||
},
|
||||
style: TextStyle(color: cs.onSurface, fontSize: 16),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Введите ID контакта',
|
||||
hintText: _mode == _SearchMode.phone
|
||||
? 'Введите номер телефона'
|
||||
: 'Введите ID контакта',
|
||||
hintStyle: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 16,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Symbols.tag,
|
||||
_mode == _SearchMode.phone ? Symbols.call : Symbols.tag,
|
||||
color: cs.onSurfaceVariant,
|
||||
size: 20,
|
||||
),
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../../backend/modules/contacts.dart';
|
||||
import '../../../core/cache/info_cache.dart';
|
||||
import '../../../core/contacts/device_contacts_service.dart';
|
||||
import '../../../core/nfc/nfc_exchange_service.dart';
|
||||
import '../../../core/storage/app_database.dart';
|
||||
import '../../../core/utils/format.dart';
|
||||
@@ -130,6 +131,11 @@ class _NfcExchangeSheetState extends State<NfcExchangeSheet>
|
||||
|
||||
String _peerName() {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final phone = _peerPhone;
|
||||
if (phone != null) {
|
||||
final book = DeviceContactsService.nameForPhone(phone);
|
||||
if (book != null) return book;
|
||||
}
|
||||
return _peerInfo?.displayName ??
|
||||
l10n.nfcPeerNameFallback('${_peerId ?? ''}');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user