feat(contacts): имена из телефонной книги везде + тумблер в дев-меню
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"/>
|
||||
<uses-permission android:name="android.permission.NFC"/>
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-feature android:name="android.hardware.nfc.hce" android:required="false"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation"/>
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
<string>Доступ к галерее нужен, чтобы отправлять фото и видео в чатах.</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>Доступ к галерее нужен, чтобы сохранять полученные фото и видео.</string>
|
||||
<key>NSContactsUsageDescription</key>
|
||||
<string>Доступ к контактам нужен, чтобы показывать имена собеседников так, как они записаны в вашей телефонной книге.</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
@@ -60,8 +60,14 @@ class PhoneLookupResult {
|
||||
final int id;
|
||||
final String? name;
|
||||
final String? avatarUrl;
|
||||
final int phone;
|
||||
|
||||
const PhoneLookupResult({required this.id, this.name, this.avatarUrl});
|
||||
const PhoneLookupResult({
|
||||
required this.id,
|
||||
this.name,
|
||||
this.avatarUrl,
|
||||
this.phone = 0,
|
||||
});
|
||||
}
|
||||
|
||||
class ContactPhotos {
|
||||
@@ -88,6 +94,14 @@ class ContactsModule {
|
||||
final id = contact['id'];
|
||||
if (id is! int) return null;
|
||||
|
||||
primeContactCache(contact);
|
||||
|
||||
final payloadPhone = contact['phone'];
|
||||
final resolvedPhone = payloadPhone is int && payloadPhone > 0
|
||||
? payloadPhone
|
||||
: int.tryParse(normalized.substring(1)) ?? 0;
|
||||
ContactCache.putPhone(id, resolvedPhone);
|
||||
|
||||
String? name;
|
||||
final names = contact['names'];
|
||||
if (names is List) {
|
||||
@@ -105,6 +119,7 @@ class ContactsModule {
|
||||
id: id,
|
||||
name: name,
|
||||
avatarUrl: contact['baseUrl'] as String?,
|
||||
phone: resolvedPhone,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -154,7 +169,7 @@ class ContactsModule {
|
||||
row['phone'] = phone;
|
||||
}
|
||||
await AppDatabase.saveContacts([row]);
|
||||
if (contact != null) _primeContactCache(contact);
|
||||
if (contact != null) primeContactCache(contact);
|
||||
revision.value++;
|
||||
return CachedContact.fromDbRow(row);
|
||||
}
|
||||
@@ -171,7 +186,7 @@ class ContactsModule {
|
||||
final contact = raw.cast<dynamic, dynamic>();
|
||||
final row = _parseContact(contact, accountId);
|
||||
if (row != null) rows.add(row);
|
||||
_primeContactCache(contact);
|
||||
primeContactCache(contact);
|
||||
}
|
||||
|
||||
if (rows.isNotEmpty) {
|
||||
@@ -200,16 +215,19 @@ class ContactsModule {
|
||||
for (final raw in contacts.whereType<Map>()) {
|
||||
if (raw['id'] != accountId) continue;
|
||||
final contact = raw.cast<dynamic, dynamic>();
|
||||
_primeContactCache(contact);
|
||||
primeContactCache(contact);
|
||||
return ProfileData.fromServerMap(contact);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static void _primeContactCache(Map<dynamic, dynamic> contact) {
|
||||
static void primeContactCache(Map<dynamic, dynamic> contact) {
|
||||
final id = contact['id'];
|
||||
if (id is! int) return;
|
||||
|
||||
final phone = contact['phone'];
|
||||
if (phone is int) ContactCache.putPhone(id, phone);
|
||||
|
||||
final names = contact['names'];
|
||||
if (names is List && names.isNotEmpty) {
|
||||
final nameRaw = names.firstWhere(
|
||||
@@ -297,6 +315,7 @@ class ContactsModule {
|
||||
static Future<void> primeCacheFromDb(int accountId) async {
|
||||
final contacts = await getContacts(accountId);
|
||||
for (final c in contacts) {
|
||||
ContactCache.putPhone(c.id, c.phone);
|
||||
final fullName = (c.lastName != null && c.lastName!.isNotEmpty)
|
||||
? '${c.firstName} ${c.lastName}'
|
||||
: c.firstName;
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../api.dart';
|
||||
import '../../core/config/komet_settings.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';
|
||||
@@ -17,6 +18,7 @@ class ContactCache {
|
||||
static final Map<int, String> _nameCache = {};
|
||||
static final Map<int, String> _avatarCache = {};
|
||||
static final Map<int, Set<String>> _optionsCache = {};
|
||||
static final Map<int, int> _phoneCache = {};
|
||||
|
||||
static const _prefsKey = 'contact_cache_v1';
|
||||
static Timer? _saveTimer;
|
||||
@@ -61,7 +63,18 @@ class ContactCache {
|
||||
_scheduleSave();
|
||||
}
|
||||
|
||||
static String? get(int id) => _nameCache[id];
|
||||
static void putPhone(int id, int phone) {
|
||||
if (phone > 0) _phoneCache[id] = phone;
|
||||
}
|
||||
|
||||
static String? get(int id) {
|
||||
final phone = _phoneCache[id];
|
||||
if (phone != null) {
|
||||
final book = DeviceContactsService.nameForPhone(phone);
|
||||
if (book != null && book.isNotEmpty) return book;
|
||||
}
|
||||
return _nameCache[id];
|
||||
}
|
||||
static String? getAvatar(int id) => _avatarCache[id];
|
||||
static Set<String>? getOptions(int id) => _optionsCache[id];
|
||||
static bool isOfficial(int id) =>
|
||||
@@ -71,6 +84,7 @@ class ContactCache {
|
||||
_nameCache.clear();
|
||||
_avatarCache.clear();
|
||||
_optionsCache.clear();
|
||||
_phoneCache.clear();
|
||||
_saveTimer?.cancel();
|
||||
_saveTimer = null;
|
||||
unawaited(_wipePersisted());
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'persisted_setting.dart';
|
||||
|
||||
class AppPhonebookNames {
|
||||
static const prefKey = 'dev_phonebook_names';
|
||||
static const bool defaultValue = true;
|
||||
|
||||
static final _setting = PersistedSetting<bool>(
|
||||
prefKey: prefKey,
|
||||
defaultValue: defaultValue,
|
||||
read: (prefs, key) => prefs.getBool(key),
|
||||
write: (prefs, key, value) async {
|
||||
await prefs.setBool(key, value);
|
||||
},
|
||||
);
|
||||
|
||||
static ValueNotifier<bool> get current => _setting.current;
|
||||
|
||||
static Future<bool> load() => _setting.load();
|
||||
|
||||
static Future<void> save(bool value) => _setting.save(value);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../config/app_phonebook_names.dart';
|
||||
|
||||
class DeviceContactsService {
|
||||
DeviceContactsService._();
|
||||
|
||||
static const _grantedKey = 'phonebook_granted';
|
||||
|
||||
static final Map<String, String> _byLast10 = {};
|
||||
static bool _loaded = false;
|
||||
|
||||
static bool get _supported => Platform.isAndroid || Platform.isIOS;
|
||||
|
||||
static String? _last10(String raw) {
|
||||
final digits = raw.replaceAll(RegExp(r'[^\d]'), '');
|
||||
if (digits.length < 10) return null;
|
||||
return digits.substring(digits.length - 10);
|
||||
}
|
||||
|
||||
static String? nameForPhone(int phone) {
|
||||
if (!AppPhonebookNames.current.value) return null;
|
||||
if (_byLast10.isEmpty) return null;
|
||||
final key = _last10(phone.toString());
|
||||
if (key == null) return null;
|
||||
final name = _byLast10[key];
|
||||
if (name == null || name.trim().isEmpty) return null;
|
||||
return name.trim();
|
||||
}
|
||||
|
||||
static Future<void> loadFromStartup() async {
|
||||
if (_loaded || !_supported) return;
|
||||
if (!AppPhonebookNames.current.value) return;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.getBool(_grantedKey) != true) return;
|
||||
final granted = await FlutterContacts.requestPermission(readonly: true);
|
||||
if (!granted) return;
|
||||
await _readBook();
|
||||
}
|
||||
|
||||
static Future<bool> ensureLoadedInteractive() async {
|
||||
if (_loaded || !_supported) return false;
|
||||
if (!AppPhonebookNames.current.value) return false;
|
||||
final granted = await FlutterContacts.requestPermission(readonly: true);
|
||||
if (!granted) return false;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_grantedKey, true);
|
||||
return _readBook();
|
||||
}
|
||||
|
||||
static Future<bool> reload() async {
|
||||
_loaded = false;
|
||||
_byLast10.clear();
|
||||
return ensureLoadedInteractive();
|
||||
}
|
||||
|
||||
static Future<bool> _readBook() async {
|
||||
try {
|
||||
final contacts = await FlutterContacts.getContacts(
|
||||
withProperties: true,
|
||||
);
|
||||
_byLast10.clear();
|
||||
for (final contact in contacts) {
|
||||
final name = contact.displayName.trim();
|
||||
if (name.isEmpty) continue;
|
||||
for (final phone in contact.phones) {
|
||||
final key = _last10(phone.number);
|
||||
if (key != null) {
|
||||
_byLast10.putIfAbsent(key, () => name);
|
||||
}
|
||||
}
|
||||
}
|
||||
_loaded = true;
|
||||
return _byLast10.isNotEmpty;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 ?? ''}');
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ import 'core/config/app_swipe_back_desktop.dart';
|
||||
import 'core/config/app_pranks.dart';
|
||||
import 'core/config/app_stories.dart';
|
||||
import 'core/config/app_commands.dart';
|
||||
import 'core/config/app_phonebook_names.dart';
|
||||
import 'core/contacts/device_contacts_service.dart';
|
||||
import 'core/config/app_link_preview.dart';
|
||||
import 'core/config/app_media_cache.dart';
|
||||
import 'core/config/app_pill_gradient.dart';
|
||||
@@ -214,6 +216,7 @@ void main(List<String> args) async {
|
||||
final pranksFuture = AppPranks.load();
|
||||
final storiesFuture = AppStories.load();
|
||||
final commandsFuture = AppCommands.load();
|
||||
final phonebookNamesFuture = AppPhonebookNames.load();
|
||||
final linkPreviewFuture = AppLinkPreview.load();
|
||||
final cacheLimitFuture = AppMediaCacheLimit.load();
|
||||
final digitalIdNativeFuture = AppDigitalIdNative.load();
|
||||
@@ -269,11 +272,13 @@ void main(List<String> args) async {
|
||||
pranksFuture,
|
||||
storiesFuture,
|
||||
commandsFuture,
|
||||
phonebookNamesFuture,
|
||||
linkPreviewFuture,
|
||||
cacheLimitFuture,
|
||||
digitalIdNativeFuture,
|
||||
showExtraInfoFuture,
|
||||
]);
|
||||
await DeviceContactsService.loadFromStartup();
|
||||
await trafficCaptureFuture;
|
||||
await debugLogFuture;
|
||||
runApp(
|
||||
|
||||
@@ -366,6 +366,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.1"
|
||||
flutter_contacts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_contacts
|
||||
sha256: "388d32cd33f16640ee169570128c933b45f3259bddbfae7a100bb49e5ffea9ae"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.9+2"
|
||||
flutter_inappwebview:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -63,6 +63,7 @@ dependencies:
|
||||
flutter_secure_storage: ^10.3.1
|
||||
package_info_plus: ^9.0.1
|
||||
mobile_scanner: ^7.2.0
|
||||
flutter_contacts: ^1.1.9+2
|
||||
cached_network_image: ^3.4.1
|
||||
flutter_cache_manager: ^3.4.1
|
||||
lottie: ^3.3.1
|
||||
|
||||
Reference in New Issue
Block a user