feat: кнопка добавить контакт

This commit is contained in:
Jganenokk
2026-07-23 18:37:55 +07:00
parent 5a3e1c8883
commit e018942102
11 changed files with 662 additions and 74 deletions
+86 -13
View File
@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
import '../../core/config/debug_test.dart'; import '../../core/config/debug_test.dart';
import '../../core/protocol/opcode_map.dart'; import '../../core/protocol/opcode_map.dart';
import '../../core/protocol/packet.dart';
import '../../core/storage/app_database.dart'; import '../../core/storage/app_database.dart';
import '../../core/utils/logger.dart'; import '../../core/utils/logger.dart';
import '../api.dart'; import '../api.dart';
@@ -79,6 +80,15 @@ class ContactPhotos {
static const empty = ContactPhotos(urls: [], total: 0); 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 { class ContactsModule {
static final ValueNotifier<int> revision = ValueNotifier<int>(0); static final ValueNotifier<int> revision = ValueNotifier<int>(0);
@@ -174,6 +184,63 @@ class ContactsModule {
return CachedContact.fromDbRow(row); return CachedContact.fromDbRow(row);
} }
static Future<AddContactResult> 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<dynamic, dynamic>()
: 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<void> syncFromLoginPayload( static Future<void> syncFromLoginPayload(
Map<dynamic, dynamic> data, Map<dynamic, dynamic> data,
int accountId, int accountId,
@@ -230,11 +297,8 @@ class ContactsModule {
final names = contact['names']; final names = contact['names'];
if (names is List && names.isNotEmpty) { if (names is List && names.isNotEmpty) {
final nameRaw = names.firstWhere( final nameRaw = _preferredNameEntry(names);
(n) => n is Map && n['type'] == 'ONEME', if (nameRaw != null) {
orElse: () => names.firstWhere((n) => n is Map, orElse: () => null),
);
if (nameRaw is Map) {
final firstName = (nameRaw['firstName'] as String?) ?? ''; final firstName = (nameRaw['firstName'] as String?) ?? '';
final lastName = nameRaw['lastName'] as String?; final lastName = nameRaw['lastName'] as String?;
final fullName = (lastName != null && lastName.isNotEmpty) 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<String, dynamic>? _parseContact( static Map<String, dynamic>? _parseContact(
Map<dynamic, dynamic> contact, Map<dynamic, dynamic> contact,
int accountId, int accountId,
@@ -338,14 +415,10 @@ class ContactsModule {
final names = contact['names']; final names = contact['names'];
if (names is List && names.isNotEmpty) { if (names is List && names.isNotEmpty) {
final nameRaw = names.firstWhere( final nameRaw = _preferredNameEntry(names);
(n) => n is Map && n['type'] == 'ONEME', if (nameRaw == null) return null;
orElse: () => names.firstWhere((n) => n is Map, orElse: () => null), firstName = (nameRaw['firstName'] as String?) ?? '';
); lastName = nameRaw['lastName'] as String?;
if (nameRaw is! Map) return null;
final name = nameRaw;
firstName = (name['firstName'] as String?) ?? '';
lastName = name['lastName'] as String?;
} }
final optionsRaw = contact['options']; final optionsRaw = contact['options'];
+2
View File
@@ -59,6 +59,7 @@ abstract class Opcode {
static const int contactMutual = 38; // Общие контакты static const int contactMutual = 38; // Общие контакты
static const int contactPhotos = 39; // Фото контакта static const int contactPhotos = 39; // Фото контакта
static const int contactSort = 40; // Сортировка контактов static const int contactSort = 40; // Сортировка контактов
static const int contactAddByPhone = 41; // Добавление контакта по номеру
static const int contactVerify = 42; // Верификация контакта static const int contactVerify = 42; // Верификация контакта
static const int removeContactPhoto = 43; // Удаление фото контакта static const int removeContactPhoto = 43; // Удаление фото контакта
static const int contactInfoByPhone = 46; // Поиск контакта по номеру static const int contactInfoByPhone = 46; // Поиск контакта по номеру
@@ -270,6 +271,7 @@ abstract class Opcode {
contactMutual: 'CONTACT_MUTUAL', contactMutual: 'CONTACT_MUTUAL',
contactPhotos: 'CONTACT_PHOTOS', contactPhotos: 'CONTACT_PHOTOS',
contactSort: 'CONTACT_SORT', contactSort: 'CONTACT_SORT',
contactAddByPhone: 'CONTACT_ADD_BY_PHONE',
contactVerify: 'CONTACT_VERIFY', contactVerify: 'CONTACT_VERIFY',
removeContactPhoto: 'REMOVE_CONTACT_PHOTO', removeContactPhoto: 'REMOVE_CONTACT_PHOTO',
contactInfoByPhone: 'CONTACT_INFO_BY_PHONE', contactInfoByPhone: 'CONTACT_INFO_BY_PHONE',
+2 -58
View File
@@ -10,6 +10,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'code_confirmation_screen.dart'; import 'code_confirmation_screen.dart';
import 'token_login_screen.dart'; import 'token_login_screen.dart';
import 'select_country_screen.dart'; import 'select_country_screen.dart';
import 'phone_input_formatter.dart';
import 'proxy_settings_sheet.dart'; import 'proxy_settings_sheet.dart';
import 'server_settings_sheet.dart'; import 'server_settings_sheet.dart';
import '../profile/spoof_screen.dart'; import '../profile/spoof_screen.dart';
@@ -873,7 +874,7 @@ class _LoginScreenState extends State<LoginScreen> {
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.digitsOnly, FilteringTextInputFormatter.digitsOnly,
_PhoneInputFormatter(_selectedCountry), PhoneInputFormatter(_selectedCountry),
], ],
style: TextStyle( style: TextStyle(
color: cs.onSurface, color: cs.onSurface,
@@ -1068,60 +1069,3 @@ class _LoginScreenState extends State<LoginScreen> {
} }
} }
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),
);
}
}
@@ -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),
);
}
}
@@ -9,6 +9,7 @@ import 'package:flutter/gestures.dart';
import 'chat_screen.dart'; import 'chat_screen.dart';
import 'search_screen.dart'; import 'search_screen.dart';
import 'create_group_flow.dart'; import 'create_group_flow.dart';
import '../contacts/add_contact_sheet.dart';
import '../../widgets/adaptive_shell.dart'; import '../../widgets/adaptive_shell.dart';
import '../../widgets/online_dot.dart'; import '../../widgets/online_dot.dart';
import '../../widgets/custom_notification.dart'; import '../../widgets/custom_notification.dart';
@@ -2835,7 +2836,14 @@ class _ChatListScreenState extends State<ChatListScreen>
const SizedBox(height: 4), const SizedBox(height: 4),
_buildFabMenuItem(Symbols.campaign, 'Создать канал'), _buildFabMenuItem(Symbols.campaign, 'Создать канал'),
const SizedBox(height: 4), const SizedBox(height: 4),
_buildFabMenuItem(Symbols.person_add, 'Создать контакт'), _buildFabMenuItem(
Symbols.person_add,
'Создать контакт',
onTap: () {
_toggleFab();
showAddContactSheet(context);
},
),
], ],
); );
} }
@@ -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<void> showAddContactSheet(BuildContext context) {
return showGeneralDialog<void>(
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<void> _pickCountry() async {
final picked = await Navigator.of(context).push<CountryName>(
MaterialPageRoute(
builder: (_) => SelectCountryScreen(
selectedCountry: _country,
countries: api.registrationCountries,
),
),
);
if (picked != null) {
setState(() {
_country = picked;
_phoneCtrl.clear();
});
}
}
Future<void> _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));
}
}
+16 -1
View File
@@ -919,5 +919,20 @@
"updateUpToDate": "You have the latest version", "updateUpToDate": "You have the latest version",
"updateCheckFailed": "Couldn't check for updates. Try again later", "updateCheckFailed": "Couldn't check for updates. Try again later",
"profileResurrecting": "Oops! The server didn't send your profile. Trying to regenerate…", "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"
} }
+48
View File
@@ -4075,6 +4075,54 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Couldn\'t regenerate your phone number. Please sign in again and report the issue to the developers'** /// **'Couldn\'t regenerate your phone number. Please sign in again and report the issue to the developers'**
String get profilePhoneRegenFailed; 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 class _AppLocalizationsDelegate
+26
View File
@@ -2122,4 +2122,30 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get profilePhoneRegenFailed => String get profilePhoneRegenFailed =>
'Couldn\'t regenerate your phone number. Please sign in again and report the issue to the developers'; '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';
} }
+26
View File
@@ -2135,4 +2135,30 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get profilePhoneRegenFailed => 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 => 'Не удалось добавить контакт';
} }
+16 -1
View File
@@ -694,5 +694,20 @@
"updateUpToDate": "Установлена актуальная версия", "updateUpToDate": "Установлена актуальная версия",
"updateCheckFailed": "Не удалось проверить обновления. Повторите позже", "updateCheckFailed": "Не удалось проверить обновления. Повторите позже",
"profileResurrecting": "Упс! Сервер не прислал profile. Попробую регенерировать…", "profileResurrecting": "Упс! Сервер не прислал profile. Попробую регенерировать…",
"profilePhoneRegenFailed": "Не удалось регенерировать данные об номере. Перезайдите и сообщите об проблеме разработчикам" "profilePhoneRegenFailed": "Не удалось регенерировать данные об номере. Перезайдите и сообщите об проблеме разработчикам",
"addContactTitle": "Новый контакт",
"addContactFirstName": "Имя",
"addContactLastName": "Фамилия (необязательно)",
"addContactSave": "Сохранить контакт",
"addContactNotFound": "{phone} не найден",
"@addContactNotFound": {
"placeholders": {
"phone": {
"type": "String"
}
}
},
"addContactNotFoundSubtitle": "Этого номера пока нет в приложении",
"addContactSearchOther": "Искать другой номер",
"addContactError": "Не удалось добавить контакт"
} }