From e018942102323937a408a0834c821fd67e53577b Mon Sep 17 00:00:00 2001 From: Jganenokk Date: Thu, 23 Jul 2026 18:37:55 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=D0=BA=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/backend/modules/contacts.dart | 99 ++++- lib/core/protocol/opcode_map.dart | 2 + lib/frontend/screens/auth/login_screen.dart | 60 +-- .../screens/auth/phone_input_formatter.dart | 58 +++ .../screens/chats/chat_list_screen.dart | 10 +- .../screens/contacts/add_contact_sheet.dart | 373 ++++++++++++++++++ lib/l10n/app_en.arb | 17 +- lib/l10n/app_localizations.dart | 48 +++ lib/l10n/app_localizations_en.dart | 26 ++ lib/l10n/app_localizations_ru.dart | 26 ++ lib/l10n/app_ru.arb | 17 +- 11 files changed, 662 insertions(+), 74 deletions(-) create mode 100644 lib/frontend/screens/auth/phone_input_formatter.dart create mode 100644 lib/frontend/screens/contacts/add_contact_sheet.dart diff --git a/lib/backend/modules/contacts.dart b/lib/backend/modules/contacts.dart index f3e05f4..c87e7b4 100644 --- a/lib/backend/modules/contacts.dart +++ b/lib/backend/modules/contacts.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import '../../core/config/debug_test.dart'; import '../../core/protocol/opcode_map.dart'; +import '../../core/protocol/packet.dart'; import '../../core/storage/app_database.dart'; import '../../core/utils/logger.dart'; import '../api.dart'; @@ -79,6 +80,15 @@ class ContactPhotos { static const empty = ContactPhotos(urls: [], total: 0); } +enum AddContactStatus { added, notFound, error } + +class AddContactResult { + final AddContactStatus status; + final CachedContact? contact; + + const AddContactResult(this.status, {this.contact}); +} + class ContactsModule { static final ValueNotifier revision = ValueNotifier(0); @@ -174,6 +184,63 @@ class ContactsModule { return CachedContact.fromDbRow(row); } + static Future addContactByPhone( + Api api, { + required String phone, + required String firstName, + String lastName = '', + }) async { + final normalized = _normalizePhone(phone); + if (normalized == null) { + return const AddContactResult(AddContactStatus.error); + } + + final Packet resp; + try { + resp = await api.sendRequest(Opcode.contactAddByPhone, { + 'phone': normalized, + 'firstName': firstName, + 'lastName': lastName, + }, silent: true); + } on PacketError catch (e) { + final key = e.errorKey ?? ''; + final notFound = + key == 'user.not.found' || + e.message.toLowerCase().contains('not found'); + return AddContactResult( + notFound ? AddContactStatus.notFound : AddContactStatus.error, + ); + } catch (_) { + return const AddContactResult(AddContactStatus.error); + } + + final data = resp.payload; + final contact = (data is Map && data['contact'] is Map) + ? (data['contact'] as Map).cast() + : null; + if (contact == null) { + return const AddContactResult(AddContactStatus.error); + } + + final profile = await AppDatabase.loadActiveProfile(); + if (profile == null) { + return const AddContactResult(AddContactStatus.error); + } + + final row = _parseContact(contact, profile.id); + if (row == null) { + return const AddContactResult(AddContactStatus.error); + } + + await AppDatabase.saveContacts([row]); + primeContactCache(contact); + revision.value++; + return AddContactResult( + AddContactStatus.added, + contact: CachedContact.fromDbRow(row), + ); + } + static Future syncFromLoginPayload( Map data, int accountId, @@ -230,11 +297,8 @@ class ContactsModule { final names = contact['names']; if (names is List && names.isNotEmpty) { - final nameRaw = names.firstWhere( - (n) => n is Map && n['type'] == 'ONEME', - orElse: () => names.firstWhere((n) => n is Map, orElse: () => null), - ); - if (nameRaw is Map) { + final nameRaw = _preferredNameEntry(names); + if (nameRaw != null) { final firstName = (nameRaw['firstName'] as String?) ?? ''; final lastName = nameRaw['lastName'] as String?; final fullName = (lastName != null && lastName.isNotEmpty) @@ -326,6 +390,19 @@ class ContactsModule { } } + static Map? _preferredNameEntry(List names) { + Map? oneme; + Map? any; + for (final n in names) { + if (n is! Map) continue; + any ??= n; + final type = n['type']; + if (type == 'CUSTOM') return n; + if (type == 'ONEME') oneme ??= n; + } + return oneme ?? any; + } + static Map? _parseContact( Map contact, int accountId, @@ -338,14 +415,10 @@ class ContactsModule { final names = contact['names']; if (names is List && names.isNotEmpty) { - final nameRaw = names.firstWhere( - (n) => n is Map && n['type'] == 'ONEME', - orElse: () => names.firstWhere((n) => n is Map, orElse: () => null), - ); - if (nameRaw is! Map) return null; - final name = nameRaw; - firstName = (name['firstName'] as String?) ?? ''; - lastName = name['lastName'] as String?; + final nameRaw = _preferredNameEntry(names); + if (nameRaw == null) return null; + firstName = (nameRaw['firstName'] as String?) ?? ''; + lastName = nameRaw['lastName'] as String?; } final optionsRaw = contact['options']; diff --git a/lib/core/protocol/opcode_map.dart b/lib/core/protocol/opcode_map.dart index 0e8511b..19b882a 100644 --- a/lib/core/protocol/opcode_map.dart +++ b/lib/core/protocol/opcode_map.dart @@ -59,6 +59,7 @@ abstract class Opcode { static const int contactMutual = 38; // Общие контакты static const int contactPhotos = 39; // Фото контакта static const int contactSort = 40; // Сортировка контактов + static const int contactAddByPhone = 41; // Добавление контакта по номеру static const int contactVerify = 42; // Верификация контакта static const int removeContactPhoto = 43; // Удаление фото контакта static const int contactInfoByPhone = 46; // Поиск контакта по номеру @@ -270,6 +271,7 @@ abstract class Opcode { contactMutual: 'CONTACT_MUTUAL', contactPhotos: 'CONTACT_PHOTOS', contactSort: 'CONTACT_SORT', + contactAddByPhone: 'CONTACT_ADD_BY_PHONE', contactVerify: 'CONTACT_VERIFY', removeContactPhoto: 'REMOVE_CONTACT_PHOTO', contactInfoByPhone: 'CONTACT_INFO_BY_PHONE', diff --git a/lib/frontend/screens/auth/login_screen.dart b/lib/frontend/screens/auth/login_screen.dart index ec3bb9c..a1eb649 100644 --- a/lib/frontend/screens/auth/login_screen.dart +++ b/lib/frontend/screens/auth/login_screen.dart @@ -10,6 +10,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'code_confirmation_screen.dart'; import 'token_login_screen.dart'; import 'select_country_screen.dart'; +import 'phone_input_formatter.dart'; import 'proxy_settings_sheet.dart'; import 'server_settings_sheet.dart'; import '../profile/spoof_screen.dart'; @@ -873,7 +874,7 @@ class _LoginScreenState extends State { keyboardType: TextInputType.phone, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, - _PhoneInputFormatter(_selectedCountry), + PhoneInputFormatter(_selectedCountry), ], style: TextStyle( color: cs.onSurface, @@ -1068,60 +1069,3 @@ class _LoginScreenState extends State { } } -class _PhoneInputFormatter extends TextInputFormatter { - final CountryName country; - _PhoneInputFormatter(this.country); - - @override - TextEditingValue formatEditUpdate( - TextEditingValue oldValue, - TextEditingValue newValue, - ) { - var text = newValue.text.replaceAll(RegExp(r'\D'), ''); - - if (newValue.text.length < oldValue.text.length) { - final oldDigits = oldValue.text.replaceAll(RegExp(r'\D'), ''); - if (text.length == oldDigits.length && text.isNotEmpty) { - text = text.substring(0, text.length - 1); - } - } - - if (text.length > country.phoneDigits) { - text = text.substring(0, country.phoneDigits); - } - - final buffer = StringBuffer(); - int digitIdx = 0; - - for (int i = 0; i < country.phoneGroupSizes.length; i++) { - if (digitIdx >= text.length) break; - - buffer.write(country.phoneGroupSeparators[i]); - - final groupSize = country.phoneGroupSizes[i]; - final remainingDigits = text.length - digitIdx; - final digitsToTake = remainingDigits < groupSize - ? remainingDigits - : groupSize; - - buffer.write(text.substring(digitIdx, digitIdx + digitsToTake)); - digitIdx += digitsToTake; - - if (digitIdx == text.length && - i < country.phoneGroupSeparators.length - 1) {} - } - - if (digitIdx == text.length && text.length == country.phoneDigits) { - if (country.phoneGroupSeparators.length > - country.phoneGroupSizes.length) { - buffer.write(country.phoneGroupSeparators.last); - } - } - - final formattedText = buffer.toString(); - return TextEditingValue( - text: formattedText, - selection: TextSelection.collapsed(offset: formattedText.length), - ); - } -} diff --git a/lib/frontend/screens/auth/phone_input_formatter.dart b/lib/frontend/screens/auth/phone_input_formatter.dart new file mode 100644 index 0000000..49b5807 --- /dev/null +++ b/lib/frontend/screens/auth/phone_input_formatter.dart @@ -0,0 +1,58 @@ +import 'package:flutter/services.dart'; + +import '../../../core/config/countries.dart'; + +class PhoneInputFormatter extends TextInputFormatter { + final CountryName country; + PhoneInputFormatter(this.country); + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + var text = newValue.text.replaceAll(RegExp(r'\D'), ''); + + if (newValue.text.length < oldValue.text.length) { + final oldDigits = oldValue.text.replaceAll(RegExp(r'\D'), ''); + if (text.length == oldDigits.length && text.isNotEmpty) { + text = text.substring(0, text.length - 1); + } + } + + if (text.length > country.phoneDigits) { + text = text.substring(0, country.phoneDigits); + } + + final buffer = StringBuffer(); + int digitIdx = 0; + + for (int i = 0; i < country.phoneGroupSizes.length; i++) { + if (digitIdx >= text.length) break; + + buffer.write(country.phoneGroupSeparators[i]); + + final groupSize = country.phoneGroupSizes[i]; + final remainingDigits = text.length - digitIdx; + final digitsToTake = remainingDigits < groupSize + ? remainingDigits + : groupSize; + + buffer.write(text.substring(digitIdx, digitIdx + digitsToTake)); + digitIdx += digitsToTake; + } + + if (digitIdx == text.length && text.length == country.phoneDigits) { + if (country.phoneGroupSeparators.length > + country.phoneGroupSizes.length) { + buffer.write(country.phoneGroupSeparators.last); + } + } + + final formattedText = buffer.toString(); + return TextEditingValue( + text: formattedText, + selection: TextSelection.collapsed(offset: formattedText.length), + ); + } +} diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index 083b7ca..8d35e38 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -9,6 +9,7 @@ import 'package:flutter/gestures.dart'; import 'chat_screen.dart'; import 'search_screen.dart'; import 'create_group_flow.dart'; +import '../contacts/add_contact_sheet.dart'; import '../../widgets/adaptive_shell.dart'; import '../../widgets/online_dot.dart'; import '../../widgets/custom_notification.dart'; @@ -2835,7 +2836,14 @@ class _ChatListScreenState extends State const SizedBox(height: 4), _buildFabMenuItem(Symbols.campaign, 'Создать канал'), const SizedBox(height: 4), - _buildFabMenuItem(Symbols.person_add, 'Создать контакт'), + _buildFabMenuItem( + Symbols.person_add, + 'Создать контакт', + onTap: () { + _toggleFab(); + showAddContactSheet(context); + }, + ), ], ); } diff --git a/lib/frontend/screens/contacts/add_contact_sheet.dart b/lib/frontend/screens/contacts/add_contact_sheet.dart new file mode 100644 index 0000000..2f91609 --- /dev/null +++ b/lib/frontend/screens/contacts/add_contact_sheet.dart @@ -0,0 +1,373 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:komet/backend/modules/contacts.dart'; +import 'package:komet/core/config/countries.dart'; +import 'package:komet/frontend/screens/auth/phone_input_formatter.dart'; +import 'package:komet/frontend/screens/auth/select_country_screen.dart'; +import 'package:komet/frontend/screens/contacts/open_contact_profile.dart'; +import 'package:komet/frontend/widgets/custom_notification.dart'; +import 'package:komet/l10n/app_localizations.dart'; +import 'package:komet/main.dart'; + +Future showAddContactSheet(BuildContext context) { + return showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, + barrierColor: Colors.black.withValues(alpha: 0.28), + transitionDuration: const Duration(milliseconds: 260), + pageBuilder: (_, _, _) => const SizedBox.shrink(), + transitionBuilder: (dialogContext, anim, _, _) { + final t = Curves.easeOutCubic.transform(anim.value); + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 14 * t, sigmaY: 14 * t), + child: Opacity( + opacity: anim.value, + child: Transform.scale( + scale: 0.94 + 0.06 * t, + child: _AddContactCard(hostContext: context), + ), + ), + ); + }, + ); +} + +class _AddContactCard extends StatefulWidget { + final BuildContext hostContext; + + const _AddContactCard({required this.hostContext}); + + @override + State<_AddContactCard> createState() => _AddContactCardState(); +} + +class _AddContactCardState extends State<_AddContactCard> { + final TextEditingController _phoneCtrl = TextEditingController(); + final TextEditingController _firstCtrl = TextEditingController(); + final TextEditingController _lastCtrl = TextEditingController(); + + late CountryName _country; + bool _loading = false; + String? _notFoundPhone; + + @override + void initState() { + super.initState(); + final allowed = api.registrationCountries; + _country = + countriesByCode['RU'] ?? + (allowed.isNotEmpty ? allowed.first : allCountries.first); + if (allowed.isNotEmpty && !allowed.any((c) => c.code == _country.code)) { + _country = allowed.first; + } + } + + @override + void dispose() { + _phoneCtrl.dispose(); + _firstCtrl.dispose(); + _lastCtrl.dispose(); + super.dispose(); + } + + String get _digits => _phoneCtrl.text.replaceAll(RegExp(r'\D'), ''); + bool get _phoneValid => _digits.length == _country.phoneDigits; + bool get _canSave => + _phoneValid && _firstCtrl.text.trim().isNotEmpty && !_loading; + + Future _pickCountry() async { + final picked = await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => SelectCountryScreen( + selectedCountry: _country, + countries: api.registrationCountries, + ), + ), + ); + if (picked != null) { + setState(() { + _country = picked; + _phoneCtrl.clear(); + }); + } + } + + Future _save() async { + if (!_canSave) return; + setState(() => _loading = true); + final phone = '${_country.phoneCode}$_digits'; + final result = await ContactsModule.addContactByPhone( + api, + phone: phone, + firstName: _firstCtrl.text.trim(), + lastName: _lastCtrl.text.trim(), + ); + if (!mounted) return; + setState(() => _loading = false); + + switch (result.status) { + case AddContactStatus.added: + final contact = result.contact; + Navigator.of(context).pop(); + if (contact == null) return; + final name = (contact.lastName != null && contact.lastName!.isNotEmpty) + ? '${contact.firstName} ${contact.lastName}' + : contact.firstName; + if (!widget.hostContext.mounted) return; + await openContactDialogProfile( + widget.hostContext, + contactId: contact.id, + name: name, + avatarUrl: contact.baseUrl, + ); + case AddContactStatus.notFound: + setState(() => _notFoundPhone = phone); + case AddContactStatus.error: + showCustomNotification( + context, + AppLocalizations.of(context)!.addContactError, + ); + } + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final width = MediaQuery.sizeOf(context).width; + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Material( + color: Colors.transparent, + child: Container( + width: width > 420 ? 380 : double.infinity, + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(22), + ), + clipBehavior: Clip.antiAlias, + child: AnimatedSize( + duration: const Duration(milliseconds: 220), + curve: Curves.easeOutCubic, + child: _notFoundPhone != null + ? _buildNotFound(cs) + : _buildForm(cs), + ), + ), + ), + ), + ); + } + + Widget _buildForm(ColorScheme cs) { + final l10n = AppLocalizations.of(context)!; + return Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.addContactTitle, + style: TextStyle( + color: cs.onSurface, + fontSize: 22, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 20), + _buildPhoneRow(cs), + _divider(cs), + _buildTextRow( + cs, + controller: _firstCtrl, + hint: l10n.addContactFirstName, + ), + _divider(cs), + _buildTextRow( + cs, + controller: _lastCtrl, + hint: l10n.addContactLastName, + ), + _divider(cs), + _buildSaveButton(cs, l10n), + ], + ), + ); + } + + Widget _buildPhoneRow(ColorScheme cs) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + children: [ + InkWell( + onTap: _loading ? null : _pickCountry, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _flagEmoji(_country.code), + style: const TextStyle(fontSize: 22), + ), + const SizedBox(width: 6), + Text( + _country.phoneCode, + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + Icon( + Icons.keyboard_arrow_down, + size: 20, + color: cs.onSurfaceVariant, + ), + ], + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: TextField( + key: ValueKey(_country.code), + controller: _phoneCtrl, + keyboardType: TextInputType.phone, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + PhoneInputFormatter(_country), + ], + onChanged: (_) => setState(() {}), + style: TextStyle(color: cs.onSurface, fontSize: 16), + decoration: InputDecoration( + isCollapsed: true, + border: InputBorder.none, + hintText: _country.phoneMask.replaceAll('#', '0'), + hintStyle: TextStyle(color: cs.outline, fontSize: 16), + ), + ), + ), + ], + ), + ); + } + + Widget _buildTextRow( + ColorScheme cs, { + required TextEditingController controller, + required String hint, + }) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + children: [ + Expanded( + child: TextField( + controller: controller, + maxLength: 60, + onChanged: (_) => setState(() {}), + style: TextStyle(color: cs.onSurface, fontSize: 16), + decoration: InputDecoration( + isCollapsed: true, + border: InputBorder.none, + counterText: '', + hintText: hint, + hintStyle: TextStyle(color: cs.onSurfaceVariant, fontSize: 16), + ), + ), + ), + const SizedBox(width: 8), + Text( + '${controller.text.characters.length}/60', + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), + ), + ], + ), + ); + } + + Widget _buildSaveButton(ColorScheme cs, AppLocalizations l10n) { + return SizedBox( + width: double.infinity, + child: TextButton( + onPressed: _canSave ? _save : null, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + foregroundColor: cs.primary, + disabledForegroundColor: cs.onSurfaceVariant.withValues(alpha: 0.5), + ), + child: _loading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: cs.primary, + ), + ) + : Text( + l10n.addContactSave, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + ), + ); + } + + Widget _buildNotFound(ColorScheme cs) { + final l10n = AppLocalizations.of(context)!; + return Padding( + padding: const EdgeInsets.fromLTRB(20, 22, 20, 12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.addContactNotFound(_notFoundPhone ?? ''), + style: TextStyle( + color: cs.onSurface, + fontSize: 21, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 6), + Text( + l10n.addContactNotFoundSubtitle, + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: FilledButton.tonal( + onPressed: () => setState(() => _notFoundPhone = null), + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + ), + child: Text(l10n.addContactSearchOther), + ), + ), + ], + ), + ); + } + + Widget _divider(ColorScheme cs) => + Divider(height: 1, thickness: 0.5, color: cs.outlineVariant); + + static String _flagEmoji(String code) { + if (code.length != 2) return '🏳️'; + final upper = code.toUpperCase(); + final a = upper.codeUnitAt(0); + final b = upper.codeUnitAt(1); + if (a < 65 || a > 90 || b < 65 || b > 90) return '🏳️'; + return String.fromCharCode(0x1F1E6 + (a - 65)) + + String.fromCharCode(0x1F1E6 + (b - 65)); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1225ca5..69fe0ba 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -919,5 +919,20 @@ "updateUpToDate": "You have the latest version", "updateCheckFailed": "Couldn't check for updates. Try again later", "profileResurrecting": "Oops! The server didn't send your profile. Trying to regenerate…", - "profilePhoneRegenFailed": "Couldn't regenerate your phone number. Please sign in again and report the issue to the developers" + "profilePhoneRegenFailed": "Couldn't regenerate your phone number. Please sign in again and report the issue to the developers", + "addContactTitle": "Add contact", + "addContactFirstName": "First name", + "addContactLastName": "Last name (optional)", + "addContactSave": "Save contact", + "addContactNotFound": "{phone} not found", + "@addContactNotFound": { + "placeholders": { + "phone": { + "type": "String" + } + } + }, + "addContactNotFoundSubtitle": "This number isn't on the app yet", + "addContactSearchOther": "Search for other number", + "addContactError": "Couldn't add contact" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index da9929e..c96cd1a 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4075,6 +4075,54 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Couldn\'t regenerate your phone number. Please sign in again and report the issue to the developers'** String get profilePhoneRegenFailed; + + /// No description provided for @addContactTitle. + /// + /// In en, this message translates to: + /// **'Add contact'** + String get addContactTitle; + + /// No description provided for @addContactFirstName. + /// + /// In en, this message translates to: + /// **'First name'** + String get addContactFirstName; + + /// No description provided for @addContactLastName. + /// + /// In en, this message translates to: + /// **'Last name (optional)'** + String get addContactLastName; + + /// No description provided for @addContactSave. + /// + /// In en, this message translates to: + /// **'Save contact'** + String get addContactSave; + + /// No description provided for @addContactNotFound. + /// + /// In en, this message translates to: + /// **'{phone} not found'** + String addContactNotFound(String phone); + + /// No description provided for @addContactNotFoundSubtitle. + /// + /// In en, this message translates to: + /// **'This number isn\'t on the app yet'** + String get addContactNotFoundSubtitle; + + /// No description provided for @addContactSearchOther. + /// + /// In en, this message translates to: + /// **'Search for other number'** + String get addContactSearchOther; + + /// No description provided for @addContactError. + /// + /// In en, this message translates to: + /// **'Couldn\'t add contact'** + String get addContactError; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 687bc06..59867d9 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2122,4 +2122,30 @@ class AppLocalizationsEn extends AppLocalizations { @override String get profilePhoneRegenFailed => 'Couldn\'t regenerate your phone number. Please sign in again and report the issue to the developers'; + + @override + String get addContactTitle => 'Add contact'; + + @override + String get addContactFirstName => 'First name'; + + @override + String get addContactLastName => 'Last name (optional)'; + + @override + String get addContactSave => 'Save contact'; + + @override + String addContactNotFound(String phone) { + return '$phone not found'; + } + + @override + String get addContactNotFoundSubtitle => 'This number isn\'t on the app yet'; + + @override + String get addContactSearchOther => 'Search for other number'; + + @override + String get addContactError => 'Couldn\'t add contact'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 3dd52f8..5e9f539 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2135,4 +2135,30 @@ class AppLocalizationsRu extends AppLocalizations { @override String get profilePhoneRegenFailed => 'Не удалось регенерировать данные об номере. Перезайдите и сообщите об проблеме разработчикам'; + + @override + String get addContactTitle => 'Новый контакт'; + + @override + String get addContactFirstName => 'Имя'; + + @override + String get addContactLastName => 'Фамилия (необязательно)'; + + @override + String get addContactSave => 'Сохранить контакт'; + + @override + String addContactNotFound(String phone) { + return '$phone не найден'; + } + + @override + String get addContactNotFoundSubtitle => 'Этого номера пока нет в приложении'; + + @override + String get addContactSearchOther => 'Искать другой номер'; + + @override + String get addContactError => 'Не удалось добавить контакт'; } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 2392538..79593f5 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -694,5 +694,20 @@ "updateUpToDate": "Установлена актуальная версия", "updateCheckFailed": "Не удалось проверить обновления. Повторите позже", "profileResurrecting": "Упс! Сервер не прислал profile. Попробую регенерировать…", - "profilePhoneRegenFailed": "Не удалось регенерировать данные об номере. Перезайдите и сообщите об проблеме разработчикам" + "profilePhoneRegenFailed": "Не удалось регенерировать данные об номере. Перезайдите и сообщите об проблеме разработчикам", + "addContactTitle": "Новый контакт", + "addContactFirstName": "Имя", + "addContactLastName": "Фамилия (необязательно)", + "addContactSave": "Сохранить контакт", + "addContactNotFound": "{phone} не найден", + "@addContactNotFound": { + "placeholders": { + "phone": { + "type": "String" + } + } + }, + "addContactNotFoundSubtitle": "Этого номера пока нет в приложении", + "addContactSearchOther": "Искать другой номер", + "addContactError": "Не удалось добавить контакт" }