From 109d9b458eac30534dd93492173145a3fc48b948 Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 7 Nov 2024 03:26:14 +0200 Subject: [PATCH] Cw 514 add sort functionality for addressbook mywallets and contacts (#1309) * add sort function to contact list * fix UI * prevent duplicate contact names * dispose contact source subscription * fix custom order issue * update the address book UI * fix saving custom order * fix merge conflict issue * review fixes [skip ci] * revert to single scroll for entire page * tabBarView address book --------- Co-authored-by: Omar Hatem --- lib/entities/contact.dart | 9 +- lib/entities/contact_record.dart | 23 +- lib/entities/preferences_key.dart | 2 + lib/entities/wallet_list_order_types.dart | 10 +- .../screens/contact/contact_list_page.dart | 388 ++++++++++++++---- .../dashboard/widgets/filter_list_widget.dart | 26 +- .../screens/wallet_list/filtered_list.dart | 46 ++- .../screens/wallet_list/wallet_list_page.dart | 2 +- .../widgets/collapsible_standart_list.dart | 39 -- lib/store/settings_store.dart | 40 +- .../contact_list/contact_list_view_model.dart | 84 +++- .../contact_list/contact_view_model.dart | 24 +- .../wallet_list/wallet_list_view_model.dart | 14 +- res/values/strings_ar.arb | 5 +- res/values/strings_bg.arb | 5 +- res/values/strings_cs.arb | 5 +- res/values/strings_en.arb | 5 +- res/values/strings_es.arb | 3 +- res/values/strings_fr.arb | 5 +- res/values/strings_ha.arb | 5 +- res/values/strings_hi.arb | 5 +- res/values/strings_hr.arb | 5 +- res/values/strings_id.arb | 5 +- res/values/strings_it.arb | 5 +- res/values/strings_ja.arb | 5 +- res/values/strings_ko.arb | 5 +- res/values/strings_my.arb | 5 +- res/values/strings_nl.arb | 5 +- res/values/strings_pl.arb | 5 +- res/values/strings_pt.arb | 2 +- res/values/strings_ru.arb | 5 +- res/values/strings_th.arb | 5 +- res/values/strings_tl.arb | 2 +- res/values/strings_tr.arb | 5 +- res/values/strings_uk.arb | 5 +- res/values/strings_ur.arb | 5 +- res/values/strings_yo.arb | 5 +- res/values/strings_zh.arb | 5 +- 38 files changed, 588 insertions(+), 236 deletions(-) delete mode 100644 lib/src/widgets/collapsible_standart_list.dart diff --git a/lib/entities/contact.dart b/lib/entities/contact.dart index cd4fa55a2..901993f43 100644 --- a/lib/entities/contact.dart +++ b/lib/entities/contact.dart @@ -7,7 +7,8 @@ part 'contact.g.dart'; @HiveType(typeId: Contact.typeId) class Contact extends HiveObject with Keyable { - Contact({required this.name, required this.address, CryptoCurrency? type}) { + Contact({required this.name, required this.address, CryptoCurrency? type, DateTime? lastChange}) + : lastChange = lastChange ?? DateTime.now() { if (type != null) { raw = type.raw; } @@ -25,6 +26,9 @@ class Contact extends HiveObject with Keyable { @HiveField(2, defaultValue: 0) late int raw; + @HiveField(3) + DateTime lastChange; + CryptoCurrency get type => CryptoCurrency.deserialize(raw: raw); @override @@ -36,6 +40,5 @@ class Contact extends HiveObject with Keyable { @override int get hashCode => key.hashCode; - void updateCryptoCurrency({required CryptoCurrency currency}) => - raw = currency.raw; + void updateCryptoCurrency({required CryptoCurrency currency}) => raw = currency.raw; } diff --git a/lib/entities/contact_record.dart b/lib/entities/contact_record.dart index 50a432727..4b0e120ba 100644 --- a/lib/entities/contact_record.dart +++ b/lib/entities/contact_record.dart @@ -1,22 +1,21 @@ +import 'package:cake_wallet/entities/contact.dart'; +import 'package:cake_wallet/entities/contact_base.dart'; +import 'package:cake_wallet/entities/record.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/entities/contact.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/entities/record.dart'; -import 'package:cake_wallet/entities/contact_base.dart'; part 'contact_record.g.dart'; class ContactRecord = ContactRecordBase with _$ContactRecord; -abstract class ContactRecordBase extends Record - with Store - implements ContactBase { +abstract class ContactRecordBase extends Record with Store implements ContactBase { ContactRecordBase(Box source, Contact original) : name = original.name, address = original.address, type = original.type, - super(source, original); + lastChange = original.lastChange, + super(source, original); @override @observable @@ -30,14 +29,14 @@ abstract class ContactRecordBase extends Record @observable CryptoCurrency type; + DateTime? lastChange; + @override void toBind(Contact original) { reaction((_) => name, (String name) => original.name = name); reaction((_) => address, (String address) => original.address = address); - reaction( - (_) => type, - (CryptoCurrency currency) => - original.updateCryptoCurrency(currency: currency)); + reaction((_) => type, + (CryptoCurrency currency) => original.updateCryptoCurrency(currency: currency)); } @override diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 4fbe358e5..73556452c 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -25,7 +25,9 @@ class PreferencesKey { static const disableBulletinKey = 'disable_bulletin'; static const defaultBuyProvider = 'default_buy_provider'; static const walletListOrder = 'wallet_list_order'; + static const contactListOrder = 'contact_list_order'; static const walletListAscending = 'wallet_list_ascending'; + static const contactListAscending = 'contact_list_ascending'; static const currentFiatApiModeKey = 'current_fiat_api_mode'; static const failedTotpTokenTrials = 'failed_token_trials'; static const disableExchangeKey = 'disable_exchange'; diff --git a/lib/entities/wallet_list_order_types.dart b/lib/entities/wallet_list_order_types.dart index f848170f4..751569e9e 100644 --- a/lib/entities/wallet_list_order_types.dart +++ b/lib/entities/wallet_list_order_types.dart @@ -1,6 +1,6 @@ import 'package:cake_wallet/generated/i18n.dart'; -enum WalletListOrderType { +enum FilterListOrderType { CreationDate, Alphabetical, GroupByType, @@ -9,13 +9,13 @@ enum WalletListOrderType { @override String toString() { switch (this) { - case WalletListOrderType.CreationDate: + case FilterListOrderType.CreationDate: return S.current.creation_date; - case WalletListOrderType.Alphabetical: + case FilterListOrderType.Alphabetical: return S.current.alphabetical; - case WalletListOrderType.GroupByType: + case FilterListOrderType.GroupByType: return S.current.group_by_type; - case WalletListOrderType.Custom: + case FilterListOrderType.Custom: return S.current.custom_drag; } } diff --git a/lib/src/screens/contact/contact_list_page.dart b/lib/src/screens/contact/contact_list_page.dart index 99f2aa251..130380b09 100644 --- a/lib/src/screens/contact/contact_list_page.dart +++ b/lib/src/screens/contact/contact_list_page.dart @@ -1,21 +1,24 @@ import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/entities/contact_base.dart'; import 'package:cake_wallet/entities/contact_record.dart'; +import 'package:cake_wallet/entities/wallet_list_order_types.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/filter_list_widget.dart'; +import 'package:cake_wallet/src/screens/wallet_list/filtered_list.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; -import 'package:cake_wallet/src/widgets/collapsible_standart_list.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; class ContactListPage extends BasePage { ContactListPage(this.contactListViewModel, this.authService); @@ -74,45 +77,101 @@ class ContactListPage extends BasePage { } @override - Widget body(BuildContext context) { - return Container( - padding: EdgeInsets.all(20.0), - child: Observer(builder: (_) { - final contacts = contactListViewModel.contactsToShow; - final walletContacts = contactListViewModel.walletContactsToShow; - return CollapsibleSectionList( - sectionCount: 2, - sectionTitleBuilder: (int sectionIndex) { - var title = S.current.contact_list_contacts; + Widget body(BuildContext context) => ContactPageBody(contactListViewModel: contactListViewModel); +} - if (sectionIndex == 0) { - title = S.current.contact_list_wallets; - } +class ContactPageBody extends StatefulWidget { + const ContactPageBody({required this.contactListViewModel}); - return Container( - padding: EdgeInsets.only(bottom: 10), - child: Text(title, style: TextStyle(fontSize: 36))); - }, - itemCounter: (int sectionIndex) => - sectionIndex == 0 ? walletContacts.length : contacts.length, - itemBuilder: (int sectionIndex, index) { - if (sectionIndex == 0) { - final walletInfo = walletContacts[index]; - return generateRaw(context, walletInfo); - } + final ContactListViewModel contactListViewModel; - final contact = contacts[index]; - final content = generateRaw(context, contact); - return contactListViewModel.isEditable - ? Slidable( - key: Key('${contact.key}'), - endActionPane: _actionPane(context, contact), - child: content, - ) - : content; - }, - ); - })); + @override + State createState() => _ContactPageBodyState(); +} + +class _ContactPageBodyState extends State with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 24), + child: Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: TabBar( + controller: _tabController, + splashFactory: NoSplash.splashFactory, + indicatorSize: TabBarIndicatorSize.label, + isScrollable: true, + labelStyle: TextStyle( + fontSize: 18, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).appBarTheme.titleTextStyle!.color, + ), + unselectedLabelStyle: TextStyle( + fontSize: 18, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).appBarTheme.titleTextStyle!.color?.withOpacity(0.5)), + labelColor: Theme.of(context).appBarTheme.titleTextStyle!.color, + indicatorColor: Theme.of(context).appBarTheme.titleTextStyle!.color, + indicatorPadding: EdgeInsets.zero, + labelPadding: EdgeInsets.only(right: 24), + tabAlignment: TabAlignment.center, + dividerColor: Colors.transparent, + padding: EdgeInsets.zero, + tabs: [ + Tab(text: S.of(context).wallets), + Tab(text: S.of(context).contact_list_contacts), + ], + ), + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildWalletContacts(context), + ContactListBody( + contactListViewModel: widget.contactListViewModel, + tabController: _tabController), + ], + ), + ), + ], + ), + ); + } + + Widget _buildWalletContacts(BuildContext context) { + final walletContacts = widget.contactListViewModel.walletContactsToShow; + + return ListView.builder( + shrinkWrap: true, + itemCount: walletContacts.length * 2, + itemBuilder: (context, index) { + if (index.isOdd) { + return StandardListSeparator(); + } else { + final walletInfo = walletContacts[index ~/ 2]; + return generateRaw(context, walletInfo); + } + }, + ); } Widget generateRaw(BuildContext context, ContactBase contact) { @@ -123,7 +182,7 @@ class ContactListPage extends BasePage { return GestureDetector( onTap: () async { - if (!contactListViewModel.isEditable) { + if (!widget.contactListViewModel.isEditable) { Navigator.of(context).pop(contact); return; } @@ -143,8 +202,7 @@ class ContactListPage extends BasePage { mainAxisAlignment: MainAxisAlignment.start, children: [ currencyIcon, - Expanded( - child: Padding( + Padding( padding: EdgeInsets.only(left: 12), child: Text( contact.name, @@ -154,13 +212,215 @@ class ContactListPage extends BasePage { color: Theme.of(context).extension()!.titleColor, ), ), - )) + ), ], ), ), ); } + Future showNameAndAddressDialog(BuildContext context, String name, String address) async { + return await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: name, + alertContent: address, + rightButtonText: S.of(context).copy, + leftButtonText: S.of(context).cancel, + actionRightButton: () => Navigator.of(context).pop(true), + actionLeftButton: () => Navigator.of(context).pop(false)); + }) ?? + false; + } +} + +class ContactListBody extends StatefulWidget { + ContactListBody({required this.contactListViewModel, required this.tabController}); + + final ContactListViewModel contactListViewModel; + final TabController tabController; + + @override + State createState() => _ContactListBodyState(); +} + +class _ContactListBodyState extends State { + bool _isContactsTabActive = false; + + @override + void initState() { + super.initState(); + widget.tabController.addListener(_handleTabChange); + } + + void _handleTabChange() { + setState(() { + _isContactsTabActive = widget.tabController.index == 1; + }); + } + + @override + void dispose() { + widget.tabController.removeListener(_handleTabChange); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final contacts = widget.contactListViewModel.contacts; + return Scaffold( + body: Container( + child: FilteredList( + list: contacts, + updateFunction: widget.contactListViewModel.reorderAccordingToContactList, + canReorder: widget.contactListViewModel.isEditable, + shrinkWrap: true, + itemBuilder: (context, index) { + final contact = contacts[index]; + final contactContent = + generateContactRaw(context, contact, contacts.length == index + 1); + return GestureDetector( + key: Key('${contact.name}'), + onTap: () async { + if (!widget.contactListViewModel.isEditable) { + Navigator.of(context).pop(contact); + return; + } + + final isCopied = + await showNameAndAddressDialog(context, contact.name, contact.address); + + if (isCopied) { + await Clipboard.setData(ClipboardData(text: contact.address)); + await showBar(context, S.of(context).copied_to_clipboard); + } + }, + behavior: HitTestBehavior.opaque, + child: widget.contactListViewModel.isEditable + ? Slidable( + key: Key('${contact.key}'), + endActionPane: _actionPane(context, contact), + child: contactContent) + : contactContent, + ); + }, + ), + ), + floatingActionButton: + _isContactsTabActive ? filterButtonWidget(context, widget.contactListViewModel) : null, + ); + } + + Widget generateContactRaw(BuildContext context, ContactRecord contact, bool isLast) { + final image = contact.type.iconPath; + final currencyIcon = image != null + ? Image.asset(image, height: 24, width: 24) + : const SizedBox(height: 24, width: 24); + return Column( + children: [ + Container( + key: Key('${contact.name}'), + padding: const EdgeInsets.only(top: 16, bottom: 16, right: 24), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + currencyIcon, + Expanded( + child: Padding( + padding: EdgeInsets.only(left: 12), + child: Text( + contact.name, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).extension()!.titleColor, + ), + ), + )) + ], + ), + ), + StandardListSeparator() + ], + ); + } + + ActionPane _actionPane(BuildContext context, ContactRecord contact) => ActionPane( + motion: const ScrollMotion(), + extentRatio: 0.4, + children: [ + SlidableAction( + onPressed: (_) async => await Navigator.of(context) + .pushNamed(Routes.addressBookAddContact, arguments: contact), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + icon: Icons.edit, + label: S.of(context).edit, + ), + SlidableAction( + onPressed: (_) async { + final isDelete = await showAlertDialog(context); + + if (isDelete) { + await widget.contactListViewModel.delete(contact); + } + }, + backgroundColor: Colors.red, + foregroundColor: Colors.white, + icon: CupertinoIcons.delete, + label: S.of(context).delete, + ), + ], + ); + + Widget filterButtonWidget(BuildContext context, ContactListViewModel contactListViewModel) { + final filterIcon = Image.asset('assets/images/filter_icon.png', + color: Theme.of(context).appBarTheme.titleTextStyle!.color); + return MergeSemantics( + child: SizedBox( + height: 58, + width: 58, + child: ButtonTheme( + minWidth: double.minPositive, + child: Semantics( + container: true, + child: GestureDetector( + onTap: () async { + await showPopUp( + context: context, + builder: (context) => FilterListWidget( + initalType: contactListViewModel.orderType, + initalAscending: contactListViewModel.ascending, + onClose: (bool ascending, FilterListOrderType type) async { + contactListViewModel.setAscending(ascending); + await contactListViewModel.setOrderType(type); + }, + ), + ); + }, + child: Semantics( + label: 'Transaction Filter', + button: true, + enabled: true, + child: Container( + height: 36, + width: 36, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).extension()!.buttonBackgroundColor, + ), + child: filterIcon, + ), + ), + ), + ), + ), + ), + ); + } + Future showAlertDialog(BuildContext context) async { return await showPopUp( context: context, @@ -190,32 +450,4 @@ class ContactListPage extends BasePage { }) ?? false; } - - ActionPane _actionPane(BuildContext context, ContactRecord contact) => ActionPane( - motion: const ScrollMotion(), - extentRatio: 0.4, - children: [ - SlidableAction( - onPressed: (_) async => await Navigator.of(context) - .pushNamed(Routes.addressBookAddContact, arguments: contact), - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - icon: Icons.edit, - label: S.of(context).edit, - ), - SlidableAction( - onPressed: (_) async { - final isDelete = await showAlertDialog(context); - - if (isDelete) { - await contactListViewModel.delete(contact); - } - }, - backgroundColor: Colors.red, - foregroundColor: Colors.white, - icon: CupertinoIcons.delete, - label: S.of(context).delete, - ), - ], - ); } diff --git a/lib/src/screens/dashboard/widgets/filter_list_widget.dart b/lib/src/screens/dashboard/widgets/filter_list_widget.dart index cda4f5f10..8e95de2af 100644 --- a/lib/src/screens/dashboard/widgets/filter_list_widget.dart +++ b/lib/src/screens/dashboard/widgets/filter_list_widget.dart @@ -18,9 +18,9 @@ class FilterListWidget extends StatefulWidget { required this.onClose, }); - final WalletListOrderType? initalType; + final FilterListOrderType? initalType; final bool initalAscending; - final Function(bool, WalletListOrderType) onClose; + final Function(bool, FilterListOrderType) onClose; @override FilterListWidgetState createState() => FilterListWidgetState(); @@ -28,7 +28,7 @@ class FilterListWidget extends StatefulWidget { class FilterListWidgetState extends State { late bool ascending; - late WalletListOrderType? type; + late FilterListOrderType? type; @override void initState() { @@ -37,7 +37,7 @@ class FilterListWidgetState extends State { type = widget.initalType; } - void setSelectedOrderType(WalletListOrderType? orderType) { + void setSelectedOrderType(FilterListOrderType? orderType) { setState(() { type = orderType; }); @@ -72,7 +72,7 @@ class FilterListWidgetState extends State { ), ), ), - if (type != WalletListOrderType.Custom) ...[ + if (type != FilterListOrderType.Custom) ...[ sectionDivider, SettingsChoicesCell( ChoicesListItem( @@ -89,10 +89,10 @@ class FilterListWidgetState extends State { ], sectionDivider, RadioListTile( - value: WalletListOrderType.CreationDate, + value: FilterListOrderType.CreationDate, groupValue: type, title: Text( - WalletListOrderType.CreationDate.toString(), + FilterListOrderType.CreationDate.toString(), style: TextStyle( color: Theme.of(context).extension()!.titleColor, fontSize: 16, @@ -104,10 +104,10 @@ class FilterListWidgetState extends State { activeColor: Theme.of(context).primaryColor, ), RadioListTile( - value: WalletListOrderType.Alphabetical, + value: FilterListOrderType.Alphabetical, groupValue: type, title: Text( - WalletListOrderType.Alphabetical.toString(), + FilterListOrderType.Alphabetical.toString(), style: TextStyle( color: Theme.of(context).extension()!.titleColor, fontSize: 16, @@ -119,10 +119,10 @@ class FilterListWidgetState extends State { activeColor: Theme.of(context).primaryColor, ), RadioListTile( - value: WalletListOrderType.GroupByType, + value: FilterListOrderType.GroupByType, groupValue: type, title: Text( - WalletListOrderType.GroupByType.toString(), + FilterListOrderType.GroupByType.toString(), style: TextStyle( color: Theme.of(context).extension()!.titleColor, fontSize: 16, @@ -134,10 +134,10 @@ class FilterListWidgetState extends State { activeColor: Theme.of(context).primaryColor, ), RadioListTile( - value: WalletListOrderType.Custom, + value: FilterListOrderType.Custom, groupValue: type, title: Text( - WalletListOrderType.Custom.toString(), + FilterListOrderType.Custom.toString(), style: TextStyle( color: Theme.of(context).extension()!.titleColor, fontSize: 16, diff --git a/lib/src/screens/wallet_list/filtered_list.dart b/lib/src/screens/wallet_list/filtered_list.dart index 63a1ae392..5316c8472 100644 --- a/lib/src/screens/wallet_list/filtered_list.dart +++ b/lib/src/screens/wallet_list/filtered_list.dart @@ -7,13 +7,17 @@ class FilteredList extends StatefulWidget { required this.list, required this.itemBuilder, required this.updateFunction, + this.canReorder = true, this.shrinkWrap = false, + this.physics, }); final ObservableList list; final Widget Function(BuildContext, int) itemBuilder; final Function updateFunction; + final bool canReorder; final bool shrinkWrap; + final ScrollPhysics? physics; @override FilteredListState createState() => FilteredListState(); @@ -22,21 +26,31 @@ class FilteredList extends StatefulWidget { class FilteredListState extends State { @override Widget build(BuildContext context) { - return Observer( - builder: (_) => ReorderableListView.builder( - shrinkWrap: widget.shrinkWrap, - physics: const BouncingScrollPhysics(), - itemBuilder: widget.itemBuilder, - itemCount: widget.list.length, - onReorder: (int oldIndex, int newIndex) { - if (oldIndex < newIndex) { - newIndex -= 1; - } - final dynamic item = widget.list.removeAt(oldIndex); - widget.list.insert(newIndex, item); - widget.updateFunction(); - }, - ), - ); + if (widget.canReorder) { + return Observer( + builder: (_) => ReorderableListView.builder( + shrinkWrap: widget.shrinkWrap, + physics: widget.physics ?? const BouncingScrollPhysics(), + itemBuilder: widget.itemBuilder, + itemCount: widget.list.length, + onReorder: (int oldIndex, int newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + final dynamic item = widget.list.removeAt(oldIndex); + widget.list.insert(newIndex, item); + widget.updateFunction(); + }, + ), + ); + } else { + return Observer( + builder: (_) => ListView.builder( + physics: widget.physics ?? const BouncingScrollPhysics(), + itemBuilder: widget.itemBuilder, + itemCount: widget.list.length, + ), + ); + } } } diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index d17534f6b..afb86cf0a 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -59,7 +59,7 @@ class WalletListPage extends BasePage { builder: (context) => FilterListWidget( initalType: walletListViewModel.orderType, initalAscending: walletListViewModel.ascending, - onClose: (bool ascending, WalletListOrderType type) async { + onClose: (bool ascending, FilterListOrderType type) async { walletListViewModel.setAscending(ascending); await walletListViewModel.setOrderType(type); }, diff --git a/lib/src/widgets/collapsible_standart_list.dart b/lib/src/widgets/collapsible_standart_list.dart deleted file mode 100644 index 83e4daee2..000000000 --- a/lib/src/widgets/collapsible_standart_list.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:cake_wallet/src/widgets/standard_list.dart'; -import 'package:flutter/material.dart'; - -class CollapsibleSectionList extends SectionStandardList { - CollapsibleSectionList( - {required int sectionCount, - required int Function(int sectionIndex) itemCounter, - required Widget Function(int sectionIndex, int itemIndex) itemBuilder, - Widget Function(int sectionIndex)? sectionTitleBuilder, - bool hasTopSeparator = false}) - : super( - hasTopSeparator: hasTopSeparator, - sectionCount: sectionCount, - itemCounter: itemCounter, - itemBuilder: itemBuilder, - sectionTitleBuilder: sectionTitleBuilder); - - @override - Widget buildTitle(List items, int sectionIndex) { - if (sectionTitleBuilder == null) { - throw Exception('Cannot to build title. sectionTitleBuilder is null'); - } - return sectionTitleBuilder!.call(sectionIndex); - } - - @override - List buildSection(int itemCount, List items, int sectionIndex) { - final List section = []; - - for (var itemIndex = 0; itemIndex < itemCount; itemIndex++) { - final item = itemBuilder(sectionIndex, itemIndex); - - section.add(StandardListSeparator()); - - section.add(item); - } - return section; - } -} diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 9f03c95c3..2eadbd8a9 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -62,9 +62,11 @@ abstract class SettingsStoreBase with Store { required bool initialAppSecure, required bool initialDisableBuy, required bool initialDisableSell, + required FilterListOrderType initialWalletListOrder, + required FilterListOrderType initialContactListOrder, required bool initialDisableBulletin, - required WalletListOrderType initialWalletListOrder, required bool initialWalletListAscending, + required bool initialContactListAscending, required FiatApiMode initialFiatMode, required bool initialAllowBiometricalAuthentication, required String initialTotpSecretKey, @@ -149,7 +151,9 @@ abstract class SettingsStoreBase with Store { disableSell = initialDisableSell, disableBulletin = initialDisableBulletin, walletListOrder = initialWalletListOrder, + contactListOrder = initialContactListOrder, walletListAscending = initialWalletListAscending, + contactListAscending = initialContactListAscending, shouldShowMarketPlaceInDashboard = initialShouldShowMarketPlaceInDashboard, exchangeStatus = initialExchangeStatus, currentTheme = initialTheme, @@ -324,14 +328,24 @@ abstract class SettingsStoreBase with Store { reaction( (_) => walletListOrder, - (WalletListOrderType walletListOrder) => + (FilterListOrderType walletListOrder) => sharedPreferences.setInt(PreferencesKey.walletListOrder, walletListOrder.index)); + reaction( + (_) => contactListOrder, + (FilterListOrderType contactListOrder) => + sharedPreferences.setInt(PreferencesKey.contactListOrder, contactListOrder.index)); + reaction( (_) => walletListAscending, (bool walletListAscending) => sharedPreferences.setBool(PreferencesKey.walletListAscending, walletListAscending)); + reaction( + (_) => contactListAscending, + (bool contactListAscending) => + sharedPreferences.setBool(PreferencesKey.contactListAscending, contactListAscending)); + reaction( (_) => autoGenerateSubaddressStatus, (AutoGenerateSubaddressStatus autoGenerateSubaddressStatus) => sharedPreferences.setInt( @@ -645,15 +659,21 @@ abstract class SettingsStoreBase with Store { @observable bool disableSell; + @observable + FilterListOrderType contactListOrder; + @observable bool disableBulletin; @observable - WalletListOrderType walletListOrder; + FilterListOrderType walletListOrder; @observable bool walletListAscending; + @observable + bool contactListAscending; + @observable bool allowBiometricalAuthentication; @@ -907,9 +927,13 @@ abstract class SettingsStoreBase with Store { final disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? false; final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false; final walletListOrder = - WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; + FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; + final contactListOrder = + FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.contactListOrder) ?? 0]; final walletListAscending = sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true; + final contactListAscending = + sharedPreferences.getBool(PreferencesKey.contactListAscending) ?? true; final currentFiatApiMode = FiatApiMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey) ?? FiatApiMode.enabled.raw); @@ -1200,6 +1224,8 @@ abstract class SettingsStoreBase with Store { initialDisableBulletin: disableBulletin, initialWalletListOrder: walletListOrder, initialWalletListAscending: walletListAscending, + initialContactListOrder: contactListOrder, + initialContactListAscending: contactListAscending, initialFiatMode: currentFiatApiMode, initialAllowBiometricalAuthentication: allowBiometricalAuthentication, initialCake2FAPresetOptions: selectedCake2FAPreset, @@ -1348,9 +1374,11 @@ abstract class SettingsStoreBase with Store { disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin; walletListOrder = - WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; + FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; + contactListOrder = + FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.contactListOrder) ?? 0]; walletListAscending = sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true; - + contactListAscending = sharedPreferences.getBool(PreferencesKey.contactListAscending) ?? true; shouldShowMarketPlaceInDashboard = sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ?? shouldShowMarketPlaceInDashboard; diff --git a/lib/view_model/contact_list/contact_list_view_model.dart b/lib/view_model/contact_list/contact_list_view_model.dart index 89192583e..0015463a5 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -1,18 +1,20 @@ import 'dart:async'; + import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; +import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/contact_base.dart'; +import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/wallet_contact.dart'; +import 'package:cake_wallet/entities/wallet_list_order_types.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/utils/mobx.dart'; +import 'package:collection/collection.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/entities/contact_record.dart'; -import 'package:cake_wallet/entities/contact.dart'; -import 'package:cake_wallet/utils/mobx.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:collection/collection.dart'; part 'contact_list_view_model.g.dart'; @@ -75,6 +77,8 @@ abstract class ContactListViewModelBase with Store { _subscription = contactSource.bindToListWithTransform( contacts, (Contact contact) => ContactRecord(contactSource, contact), initialFire: true); + + setOrderType(settingsStore.contactListOrder); } String _createName(String walletName, String label, {int? key = null}) { @@ -93,6 +97,10 @@ abstract class ContactListViewModelBase with Store { bool get isEditable => _currency == null; + FilterListOrderType? get orderType => settingsStore.contactListOrder; + + bool get ascending => settingsStore.contactListAscending; + @computed bool get shouldRequireTOTP2FAForAddingContacts => settingsStore.shouldRequireTOTP2FAForAddingContacts; @@ -118,4 +126,70 @@ abstract class ContactListViewModelBase with Store { _currency?.toString() == element.type.tag || _currency?.tag == element.type.toString(); } + + void dispose() async { + _subscription?.cancel(); + final List contactsSourceCopy = contacts.map((e) => e.original).toList(); + await reorderContacts(contactsSourceCopy); + } + + void reorderAccordingToContactList() => + settingsStore.contactListOrder = FilterListOrderType.Custom; + + Future reorderContacts(List contactCopy) async { + await contactSource.deleteAll(contactCopy.map((e) => e.key).toList()); + await contactSource.addAll(contactCopy); + } + + Future sortGroupByType() async { + List contactsSourceCopy = contactSource.values.toList(); + + contactsSourceCopy.sort((a, b) => ascending + ? a.type.toString().compareTo(b.type.toString()) + : b.type.toString().compareTo(a.type.toString())); + + await reorderContacts(contactsSourceCopy); + } + + Future sortAlphabetically() async { + List contactsSourceCopy = contactSource.values.toList(); + + contactsSourceCopy + .sort((a, b) => ascending ? a.name.compareTo(b.name) : b.name.compareTo(a.name)); + + await reorderContacts(contactsSourceCopy); + } + + Future sortByCreationDate() async { + List contactsSourceCopy = contactSource.values.toList(); + + contactsSourceCopy.sort((a, b) => + ascending ? a.lastChange.compareTo(b.lastChange) : b.lastChange.compareTo(a.lastChange)); + + await reorderContacts(contactsSourceCopy); + } + + void setAscending(bool ascending) => settingsStore.contactListAscending = ascending; + + Future setOrderType(FilterListOrderType? type) async { + if (type == null) return; + + settingsStore.contactListOrder = type; + + switch (type) { + case FilterListOrderType.CreationDate: + await sortByCreationDate(); + break; + case FilterListOrderType.Alphabetical: + await sortAlphabetically(); + break; + case FilterListOrderType.GroupByType: + await sortGroupByType(); + break; + case FilterListOrderType.Custom: + default: + reorderAccordingToContactList(); + break; + } + } } diff --git a/lib/view_model/contact_list/contact_view_model.dart b/lib/view_model/contact_list/contact_view_model.dart index 053cfe4c5..93abfb11c 100644 --- a/lib/view_model/contact_list/contact_view_model.dart +++ b/lib/view_model/contact_list/contact_view_model.dart @@ -2,7 +2,7 @@ import 'package:cake_wallet/entities/contact_record.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/core/execution_state.dart'; -import 'package:cw_core/wallet_base.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/entities/contact.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -17,7 +17,9 @@ abstract class ContactViewModelBase with Store { _contact = contact, name = contact?.name ?? '', address = contact?.address ?? '', - currency = contact?.type; + currency = contact?.type, + lastChange = contact?.lastChange; + @observable ExecutionState state; @@ -31,6 +33,8 @@ abstract class ContactViewModelBase with Store { @observable CryptoCurrency? currency; + DateTime? lastChange; + @computed bool get isReady => name.isNotEmpty && @@ -51,20 +55,32 @@ abstract class ContactViewModelBase with Store { Future save() async { try { state = IsExecutingState(); + final now = DateTime.now(); + + if (doesContactNameExist(name)) { + state = FailureState(S.current.contact_name_exists); + return; + } if (_contact != null && _contact!.original.isInBox) { _contact?.name = name; _contact?.address = address; _contact?.type = currency!; + _contact?.lastChange = now; await _contact?.save(); } else { await _contacts - .add(Contact(name: name, address: address, type: currency!)); + .add(Contact(name: name, address: address, type: currency!, lastChange: now)); } + lastChange = now; state = ExecutedSuccessfullyState(); } catch (e) { state = FailureState(e.toString()); } } -} + + bool doesContactNameExist(String name) { + return _contacts.values.any((contact) => contact.name == name); + } +} \ No newline at end of file diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 0ae6b75a5..725af843f 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -76,7 +76,7 @@ abstract class WalletListViewModelBase with Store { await _appStore.changeCurrentWallet(wallet); } - WalletListOrderType? get orderType => _appStore.settingsStore.walletListOrder; + FilterListOrderType? get orderType => _appStore.settingsStore.walletListOrder; bool get ascending => _appStore.settingsStore.walletListAscending; @@ -108,7 +108,7 @@ abstract class WalletListViewModelBase with Store { return; } - _appStore.settingsStore.walletListOrder = WalletListOrderType.Custom; + _appStore.settingsStore.walletListOrder = FilterListOrderType.Custom; // make a copy of the walletInfoSource: List walletInfoSourceCopy = _walletInfoSource.values.toList(); @@ -186,22 +186,22 @@ abstract class WalletListViewModelBase with Store { _appStore.settingsStore.walletListAscending = ascending; } - Future setOrderType(WalletListOrderType? type) async { + Future setOrderType(FilterListOrderType? type) async { if (type == null) return; _appStore.settingsStore.walletListOrder = type; switch (type) { - case WalletListOrderType.CreationDate: + case FilterListOrderType.CreationDate: await sortByCreationDate(); break; - case WalletListOrderType.Alphabetical: + case FilterListOrderType.Alphabetical: await sortAlphabetically(); break; - case WalletListOrderType.GroupByType: + case FilterListOrderType.GroupByType: await sortGroupByType(); break; - case WalletListOrderType.Custom: + case FilterListOrderType.Custom: default: await reorderAccordingToWalletList(); break; diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 81fe3cc2c..c2be1504a 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -937,5 +937,6 @@ "you_pay": "انت تدفع", "you_will_get": "حول الى", "you_will_send": "تحويل من", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": " .ﻒﻠﺘﺨﻣ ﻢﺳﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ .ﻞﻌﻔﻟﺎﺑ ﺓﺩﻮﺟﻮﻣ ﻢﺳﻻﺍ ﺍﺬﻬﺑ ﻝﺎﺼﺗﺍ ﺔﻬﺟ" +} diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 50db1610a..8b68d0d1e 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -937,5 +937,6 @@ "you_pay": "Вие плащате", "you_will_get": "Обръщане в", "you_will_send": "Обръщане от", - "yy": "гг" -} \ No newline at end of file + "yy": "гг", + "contact_name_exists": "Вече съществува контакт с това име. Моля, изберете друго име." +} diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index ddc91340b..9b48ebdc7 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -937,5 +937,6 @@ "you_pay": "Zaplatíte", "you_will_get": "Směnit na", "you_will_send": "Směnit z", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "Kontakt s tímto jménem již existuje. Vyberte prosím jiný název." +} diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index d6a0ee9af..4f5fd23b3 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -940,5 +940,6 @@ "you_pay": "You Pay", "you_will_get": "Convert to", "you_will_send": "Convert from", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "A contact with that name already exists. Please choose a different name." +} diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 25c9f95c1..fd20a8b30 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -938,5 +938,6 @@ "you_pay": "Tú pagas", "you_will_get": "Convertir a", "you_will_send": "Convertir de", - "yy": "YY" + "yy": "YY", + "contact_name_exists": "Ya existe un contacto con ese nombre. Elija un nombre diferente." } diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index be5b48dd8..50aceb2f1 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -937,5 +937,6 @@ "you_pay": "Vous payez", "you_will_get": "Convertir vers", "you_will_send": "Convertir depuis", - "yy": "AA" -} \ No newline at end of file + "yy": "AA", + "contact_name_exists": "Un contact portant ce nom existe déjà. Veuillez choisir un autre nom." +} diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 4deb0df1d..e66859531 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -939,5 +939,6 @@ "you_pay": "Ka Bayar", "you_will_get": "Maida zuwa", "you_will_send": "Maida daga", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "An riga an sami lamba tare da wannan sunan. Da fatan za a zaɓi suna daban." +} diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 5161250fc..bc8b81211 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -939,5 +939,6 @@ "you_pay": "आप भुगतान करते हैं", "you_will_get": "में बदलें", "you_will_send": "से रूपांतरित करें", - "yy": "वाईवाई" -} \ No newline at end of file + "yy": "वाईवाई", + "contact_name_exists": "उस नाम का एक संपर्क पहले से मौजूद है. कृपया कोई भिन्न नाम चुनें." +} diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 8ef92aaf0..9ff57707d 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -937,5 +937,6 @@ "you_pay": "Vi plaćate", "you_will_get": "Razmijeni u", "you_will_send": "Razmijeni iz", - "yy": "GG" -} \ No newline at end of file + "yy": "GG", + "contact_name_exists": "Kontakt s tim imenom već postoji. Odaberite drugo ime." +} diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 5f93082ec..2858fa193 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -940,5 +940,6 @@ "you_pay": "Anda Membayar", "you_will_get": "Konversi ke", "you_will_send": "Konversi dari", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "Kontak dengan nama tersebut sudah ada. Silakan pilih nama lain." +} diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 08ae928af..034376a6c 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -940,5 +940,6 @@ "you_pay": "Tu paghi", "you_will_get": "Converti a", "you_will_send": "Conveti da", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "Esiste già un contatto con quel nome. Scegli un nome diverso." +} diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index d70eca31b..7f0792063 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -938,5 +938,6 @@ "you_pay": "あなたが支払う", "you_will_get": "に変換", "you_will_send": "から変換", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "その名前の連絡先はすでに存在します。別の名前を選択してください。" +} diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 133ca1838..6ba402ce3 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -939,5 +939,6 @@ "you_will_get": "로 변환하다", "you_will_send": "다음에서 변환", "YY": "YY", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "해당 이름을 가진 연락처가 이미 존재합니다. 다른 이름을 선택하세요." +} diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 1727f0d71..69b888b5a 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -937,5 +937,6 @@ "you_pay": "သင်ပေးချေပါ။", "you_will_get": "သို့ပြောင်းပါ။", "you_will_send": "မှပြောင်းပါ။", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "ထိုအမည်နှင့် အဆက်အသွယ်တစ်ခု ရှိနှင့်ပြီးဖြစ်သည်။ အခြားအမည်တစ်ခုကို ရွေးပါ။" +} diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 3f2df531b..2c259bf80 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -938,5 +938,6 @@ "you_pay": "U betaalt", "you_will_get": "Converteren naar", "you_will_send": "Converteren van", - "yy": "JJ" -} \ No newline at end of file + "yy": "JJ", + "contact_name_exists": "Er bestaat al een contact met die naam. Kies een andere naam." +} diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 91b265144..936339708 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -937,5 +937,6 @@ "you_pay": "Płacisz", "you_will_get": "Konwertuj na", "you_will_send": "Konwertuj z", - "yy": "RR" -} \ No newline at end of file + "yy": "RR", + "contact_name_exists": "Kontakt o tej nazwie już istnieje. Proszę wybrać inną nazwę." +} diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 524dbcace..0cc0cc00a 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -941,4 +941,4 @@ "you_will_get": "Converter para", "you_will_send": "Converter de", "yy": "aa" -} \ No newline at end of file +} diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 1a8c2447f..8aeb56a86 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -938,5 +938,6 @@ "you_pay": "Вы платите", "you_will_get": "Конвертировать в", "you_will_send": "Конвертировать из", - "yy": "ГГ" -} \ No newline at end of file + "yy": "ГГ", + "contact_name_exists": "Контакт с таким именем уже существует. Пожалуйста, выберите другое имя." +} diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 213f74530..736eb8762 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -937,5 +937,6 @@ "you_pay": "คุณจ่าย", "you_will_get": "แปลงเป็น", "you_will_send": "แปลงจาก", - "yy": "ปี" -} \ No newline at end of file + "yy": "ปี", + "contact_name_exists": "มีผู้ติดต่อชื่อนั้นอยู่แล้ว โปรดเลือกชื่ออื่น" +} diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 0ca8ee665..e68bd0b22 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -938,4 +938,4 @@ "you_will_get": "I-convert sa", "you_will_send": "I-convert mula sa", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index b23f64d60..83e8f6cbc 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -937,5 +937,6 @@ "you_pay": "Şu kadar ödeyeceksin: ", "you_will_get": "Biçimine dönüştür:", "you_will_send": "Biçiminden dönüştür:", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "Bu isimde bir kişi zaten mevcut. Lütfen farklı bir ad seçin." +} diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 79dc0543f..909d9c01f 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -938,5 +938,6 @@ "you_pay": "Ви платите", "you_will_get": "Конвертувати в", "you_will_send": "Конвертувати з", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "Контакт із такою назвою вже існує. Виберіть інше ім'я." +} diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 0a136d140..2dbc97102 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -939,5 +939,6 @@ "you_pay": "تم ادا کرو", "you_will_get": "میں تبدیل کریں۔", "you_will_send": "سے تبدیل کریں۔", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": " ۔ﮟﯾﺮﮐ ﺐﺨﺘﻨﻣ ﻡﺎﻧ ﻒﻠﺘﺨﻣ ﮏﯾﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔ﮯﮨ ﺩﻮﺟﻮﻣ ﮯﺳ ﮯﻠﮩﭘ ﮧﻄﺑﺍﺭ ﮏﯾﺍ ﮫﺗﺎﺳ ﮯﮐ ﻡﺎﻧ ﺱﺍ" +} diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 14270120c..03c7c95ef 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -938,5 +938,6 @@ "you_pay": "Ẹ sàn", "you_will_get": "Ṣe pàṣípààrọ̀ sí", "you_will_send": "Ṣe pàṣípààrọ̀ láti", - "yy": "Ọd" -} \ No newline at end of file + "yy": "Ọd", + "contact_name_exists": "Olubasọrọ pẹlu orukọ yẹn ti wa tẹlẹ. Jọwọ yan orukọ ti o yatọ." +} diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 65047b4fe..5c8d708f4 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -937,5 +937,6 @@ "you_pay": "你付钱", "you_will_get": "转换到", "you_will_send": "转换自", - "yy": "YY" -} \ No newline at end of file + "yy": "YY", + "contact_name_exists": "已存在具有该名称的联系人。请选择不同的名称。" +}