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 <omarh.ismail1@gmail.com>

Serhii committed Nov 7, 2024 at 03:26 UTC 109d9b458eac30534dd93492173145a3fc48b948
38 files changed +575 -223
lib/entities/contact.dart
+6 -3
@@ -7,7 +7,8 @@ part 'contact.g.dart';
7
8 @HiveType(typeId: Contact.typeId)
9 class Contact extends HiveObject with Keyable {
10 - Contact({required this.name, required this.address, CryptoCurrency? type}) {
10 + Contact({required this.name, required this.address, CryptoCurrency? type, DateTime? lastChange})
11 + : lastChange = lastChange ?? DateTime.now() {
12 if (type != null) {
13 raw = type.raw;
14 }
@@ -25,6 +26,9 @@ class Contact extends HiveObject with Keyable {
26 @HiveField(2, defaultValue: 0)
27 late int raw;
28
29 + @HiveField(3)
30 + DateTime lastChange;
31 +
32 CryptoCurrency get type => CryptoCurrency.deserialize(raw: raw);
33
34 @override
@@ -36,6 +40,5 @@ class Contact extends HiveObject with Keyable {
40 @override
41 int get hashCode => key.hashCode;
42
39 - void updateCryptoCurrency({required CryptoCurrency currency}) =>
40 - raw = currency.raw;
43 + void updateCryptoCurrency({required CryptoCurrency currency}) => raw = currency.raw;
44 }
lib/entities/contact_record.dart
+11 -12
@@ -1,22 +1,21 @@
1 -import 'package:hive/hive.dart';
2 -import 'package:mobx/mobx.dart';
1 import 'package:cake_wallet/entities/contact.dart';
4 -import 'package:cw_core/crypto_currency.dart';
5 -import 'package:cake_wallet/entities/record.dart';
2 import 'package:cake_wallet/entities/contact_base.dart';
3 +import 'package:cake_wallet/entities/record.dart';
4 +import 'package:cw_core/crypto_currency.dart';
5 +import 'package:hive/hive.dart';
6 +import 'package:mobx/mobx.dart';
7
8 part 'contact_record.g.dart';
9
10 class ContactRecord = ContactRecordBase with _$ContactRecord;
11
12 -abstract class ContactRecordBase extends Record<Contact>
13 - with Store
14 - implements ContactBase {
12 +abstract class ContactRecordBase extends Record<Contact> with Store implements ContactBase {
13 ContactRecordBase(Box<Contact> source, Contact original)
14 : name = original.name,
15 address = original.address,
16 type = original.type,
19 - super(source, original);
17 + lastChange = original.lastChange,
18 + super(source, original);
19
20 @override
21 @observable
@@ -30,14 +29,14 @@ abstract class ContactRecordBase extends Record<Contact>
29 @observable
30 CryptoCurrency type;
31
32 + DateTime? lastChange;
33 +
34 @override
35 void toBind(Contact original) {
36 reaction((_) => name, (String name) => original.name = name);
37 reaction((_) => address, (String address) => original.address = address);
37 - reaction(
38 - (_) => type,
39 - (CryptoCurrency currency) =>
40 - original.updateCryptoCurrency(currency: currency));
38 + reaction((_) => type,
39 + (CryptoCurrency currency) => original.updateCryptoCurrency(currency: currency));
40 }
41
42 @override
lib/entities/preferences_key.dart
+2
@@ -25,7 +25,9 @@ class PreferencesKey {
25 static const disableBulletinKey = 'disable_bulletin';
26 static const defaultBuyProvider = 'default_buy_provider';
27 static const walletListOrder = 'wallet_list_order';
28 + static const contactListOrder = 'contact_list_order';
29 static const walletListAscending = 'wallet_list_ascending';
30 + static const contactListAscending = 'contact_list_ascending';
31 static const currentFiatApiModeKey = 'current_fiat_api_mode';
32 static const failedTotpTokenTrials = 'failed_token_trials';
33 static const disableExchangeKey = 'disable_exchange';
lib/entities/wallet_list_order_types.dart
+5 -5
@@ -1,6 +1,6 @@
1 import 'package:cake_wallet/generated/i18n.dart';
2
3 -enum WalletListOrderType {
3 +enum FilterListOrderType {
4 CreationDate,
5 Alphabetical,
6 GroupByType,
@@ -9,13 +9,13 @@ enum WalletListOrderType {
9 @override
10 String toString() {
11 switch (this) {
12 - case WalletListOrderType.CreationDate:
12 + case FilterListOrderType.CreationDate:
13 return S.current.creation_date;
14 - case WalletListOrderType.Alphabetical:
14 + case FilterListOrderType.Alphabetical:
15 return S.current.alphabetical;
16 - case WalletListOrderType.GroupByType:
16 + case FilterListOrderType.GroupByType:
17 return S.current.group_by_type;
18 - case WalletListOrderType.Custom:
18 + case FilterListOrderType.Custom:
19 return S.current.custom_drag;
20 }
21 }
lib/src/screens/contact/contact_list_page.dart
+297 -65
@@ -1,21 +1,24 @@
1 import 'package:cake_wallet/core/auth_service.dart';
2 import 'package:cake_wallet/entities/contact_base.dart';
3 import 'package:cake_wallet/entities/contact_record.dart';
4 +import 'package:cake_wallet/entities/wallet_list_order_types.dart';
5 +import 'package:cake_wallet/generated/i18n.dart';
6 +import 'package:cake_wallet/routes.dart';
7 +import 'package:cake_wallet/src/screens/base_page.dart';
8 +import 'package:cake_wallet/src/screens/dashboard/widgets/filter_list_widget.dart';
9 +import 'package:cake_wallet/src/screens/wallet_list/filtered_list.dart';
10 +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
11 +import 'package:cake_wallet/src/widgets/standard_list.dart';
12 import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
13 import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
14 import 'package:cake_wallet/utils/show_bar.dart';
15 import 'package:cake_wallet/utils/show_pop_up.dart';
8 -import 'package:flutter/material.dart';
16 +import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
17 import 'package:flutter/cupertino.dart';
18 +import 'package:flutter/material.dart';
19 import 'package:flutter/services.dart';
11 -import 'package:flutter_mobx/flutter_mobx.dart';
20 +import 'package:flutter/widgets.dart';
21 import 'package:flutter_slidable/flutter_slidable.dart';
13 -import 'package:cake_wallet/routes.dart';
14 -import 'package:cake_wallet/generated/i18n.dart';
15 -import 'package:cake_wallet/src/screens/base_page.dart';
16 -import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
17 -import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
18 -import 'package:cake_wallet/src/widgets/collapsible_standart_list.dart';
22
23 class ContactListPage extends BasePage {
24 ContactListPage(this.contactListViewModel, this.authService);
@@ -74,45 +77,101 @@ class ContactListPage extends BasePage {
77 }
78
79 @override
77 - Widget body(BuildContext context) {
78 - return Container(
79 - padding: EdgeInsets.all(20.0),
80 - child: Observer(builder: (_) {
81 - final contacts = contactListViewModel.contactsToShow;
82 - final walletContacts = contactListViewModel.walletContactsToShow;
83 - return CollapsibleSectionList(
84 - sectionCount: 2,
85 - sectionTitleBuilder: (int sectionIndex) {
86 - var title = S.current.contact_list_contacts;
87 -
88 - if (sectionIndex == 0) {
89 - title = S.current.contact_list_wallets;
90 - }
80 + Widget body(BuildContext context) => ContactPageBody(contactListViewModel: contactListViewModel);
81 +}
82
92 - return Container(
93 - padding: EdgeInsets.only(bottom: 10),
94 - child: Text(title, style: TextStyle(fontSize: 36)));
95 - },
96 - itemCounter: (int sectionIndex) =>
97 - sectionIndex == 0 ? walletContacts.length : contacts.length,
98 - itemBuilder: (int sectionIndex, index) {
99 - if (sectionIndex == 0) {
100 - final walletInfo = walletContacts[index];
101 - return generateRaw(context, walletInfo);
102 - }
83 +class ContactPageBody extends StatefulWidget {
84 + const ContactPageBody({required this.contactListViewModel});
85
104 - final contact = contacts[index];
105 - final content = generateRaw(context, contact);
106 - return contactListViewModel.isEditable
107 - ? Slidable(
108 - key: Key('${contact.key}'),
109 - endActionPane: _actionPane(context, contact),
110 - child: content,
111 - )
112 - : content;
113 - },
114 - );
115 - }));
86 + final ContactListViewModel contactListViewModel;
87 +
88 + @override
89 + State<ContactPageBody> createState() => _ContactPageBodyState();
90 +}
91 +
92 +class _ContactPageBodyState extends State<ContactPageBody> with SingleTickerProviderStateMixin {
93 + late TabController _tabController;
94 +
95 + @override
96 + void initState() {
97 + super.initState();
98 + _tabController = TabController(length: 2, vsync: this);
99 + }
100 +
101 + @override
102 + void dispose() {
103 + _tabController.dispose();
104 + super.dispose();
105 + }
106 +
107 + @override
108 + Widget build(BuildContext context) {
109 + return Padding(
110 + padding: const EdgeInsets.only(left: 24),
111 + child: Column(
112 + children: [
113 + Align(
114 + alignment: Alignment.centerLeft,
115 + child: TabBar(
116 + controller: _tabController,
117 + splashFactory: NoSplash.splashFactory,
118 + indicatorSize: TabBarIndicatorSize.label,
119 + isScrollable: true,
120 + labelStyle: TextStyle(
121 + fontSize: 18,
122 + fontFamily: 'Lato',
123 + fontWeight: FontWeight.w600,
124 + color: Theme.of(context).appBarTheme.titleTextStyle!.color,
125 + ),
126 + unselectedLabelStyle: TextStyle(
127 + fontSize: 18,
128 + fontFamily: 'Lato',
129 + fontWeight: FontWeight.w600,
130 + color: Theme.of(context).appBarTheme.titleTextStyle!.color?.withOpacity(0.5)),
131 + labelColor: Theme.of(context).appBarTheme.titleTextStyle!.color,
132 + indicatorColor: Theme.of(context).appBarTheme.titleTextStyle!.color,
133 + indicatorPadding: EdgeInsets.zero,
134 + labelPadding: EdgeInsets.only(right: 24),
135 + tabAlignment: TabAlignment.center,
136 + dividerColor: Colors.transparent,
137 + padding: EdgeInsets.zero,
138 + tabs: [
139 + Tab(text: S.of(context).wallets),
140 + Tab(text: S.of(context).contact_list_contacts),
141 + ],
142 + ),
143 + ),
144 + Expanded(
145 + child: TabBarView(
146 + controller: _tabController,
147 + children: [
148 + _buildWalletContacts(context),
149 + ContactListBody(
150 + contactListViewModel: widget.contactListViewModel,
151 + tabController: _tabController),
152 + ],
153 + ),
154 + ),
155 + ],
156 + ),
157 + );
158 + }
159 +
160 + Widget _buildWalletContacts(BuildContext context) {
161 + final walletContacts = widget.contactListViewModel.walletContactsToShow;
162 +
163 + return ListView.builder(
164 + shrinkWrap: true,
165 + itemCount: walletContacts.length * 2,
166 + itemBuilder: (context, index) {
167 + if (index.isOdd) {
168 + return StandardListSeparator();
169 + } else {
170 + final walletInfo = walletContacts[index ~/ 2];
171 + return generateRaw(context, walletInfo);
172 + }
173 + },
174 + );
175 }
176
177 Widget generateRaw(BuildContext context, ContactBase contact) {
@@ -123,7 +182,7 @@ class ContactListPage extends BasePage {
182
183 return GestureDetector(
184 onTap: () async {
126 - if (!contactListViewModel.isEditable) {
185 + if (!widget.contactListViewModel.isEditable) {
186 Navigator.of(context).pop(contact);
187 return;
188 }
@@ -143,8 +202,7 @@ class ContactListPage extends BasePage {
202 mainAxisAlignment: MainAxisAlignment.start,
203 children: <Widget>[
204 currencyIcon,
146 - Expanded(
147 - child: Padding(
205 + Padding(
206 padding: EdgeInsets.only(left: 12),
207 child: Text(
208 contact.name,
@@ -154,28 +212,13 @@ class ContactListPage extends BasePage {
212 color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
213 ),
214 ),
157 - ))
215 + ),
216 ],
217 ),
218 ),
219 );
220 }
221
164 - Future<bool> showAlertDialog(BuildContext context) async {
165 - return await showPopUp<bool>(
166 - context: context,
167 - builder: (BuildContext context) {
168 - return AlertWithTwoActions(
169 - alertTitle: S.of(context).address_remove_contact,
170 - alertContent: S.of(context).address_remove_content,
171 - rightButtonText: S.of(context).remove,
172 - leftButtonText: S.of(context).cancel,
173 - actionRightButton: () => Navigator.of(context).pop(true),
174 - actionLeftButton: () => Navigator.of(context).pop(false));
175 - }) ??
176 - false;
177 - }
178 -
222 Future<bool> showNameAndAddressDialog(BuildContext context, String name, String address) async {
223 return await showPopUp<bool>(
224 context: context,
@@ -190,6 +233,119 @@ class ContactListPage extends BasePage {
233 }) ??
234 false;
235 }
236 +}
237 +
238 +class ContactListBody extends StatefulWidget {
239 + ContactListBody({required this.contactListViewModel, required this.tabController});
240 +
241 + final ContactListViewModel contactListViewModel;
242 + final TabController tabController;
243 +
244 + @override
245 + State<ContactListBody> createState() => _ContactListBodyState();
246 +}
247 +
248 +class _ContactListBodyState extends State<ContactListBody> {
249 + bool _isContactsTabActive = false;
250 +
251 + @override
252 + void initState() {
253 + super.initState();
254 + widget.tabController.addListener(_handleTabChange);
255 + }
256 +
257 + void _handleTabChange() {
258 + setState(() {
259 + _isContactsTabActive = widget.tabController.index == 1;
260 + });
261 + }
262 +
263 + @override
264 + void dispose() {
265 + widget.tabController.removeListener(_handleTabChange);
266 + super.dispose();
267 + }
268 +
269 + @override
270 + Widget build(BuildContext context) {
271 + final contacts = widget.contactListViewModel.contacts;
272 + return Scaffold(
273 + body: Container(
274 + child: FilteredList(
275 + list: contacts,
276 + updateFunction: widget.contactListViewModel.reorderAccordingToContactList,
277 + canReorder: widget.contactListViewModel.isEditable,
278 + shrinkWrap: true,
279 + itemBuilder: (context, index) {
280 + final contact = contacts[index];
281 + final contactContent =
282 + generateContactRaw(context, contact, contacts.length == index + 1);
283 + return GestureDetector(
284 + key: Key('${contact.name}'),
285 + onTap: () async {
286 + if (!widget.contactListViewModel.isEditable) {
287 + Navigator.of(context).pop(contact);
288 + return;
289 + }
290 +
291 + final isCopied =
292 + await showNameAndAddressDialog(context, contact.name, contact.address);
293 +
294 + if (isCopied) {
295 + await Clipboard.setData(ClipboardData(text: contact.address));
296 + await showBar<void>(context, S.of(context).copied_to_clipboard);
297 + }
298 + },
299 + behavior: HitTestBehavior.opaque,
300 + child: widget.contactListViewModel.isEditable
301 + ? Slidable(
302 + key: Key('${contact.key}'),
303 + endActionPane: _actionPane(context, contact),
304 + child: contactContent)
305 + : contactContent,
306 + );
307 + },
308 + ),
309 + ),
310 + floatingActionButton:
311 + _isContactsTabActive ? filterButtonWidget(context, widget.contactListViewModel) : null,
312 + );
313 + }
314 +
315 + Widget generateContactRaw(BuildContext context, ContactRecord contact, bool isLast) {
316 + final image = contact.type.iconPath;
317 + final currencyIcon = image != null
318 + ? Image.asset(image, height: 24, width: 24)
319 + : const SizedBox(height: 24, width: 24);
320 + return Column(
321 + children: [
322 + Container(
323 + key: Key('${contact.name}'),
324 + padding: const EdgeInsets.only(top: 16, bottom: 16, right: 24),
325 + child: Row(
326 + mainAxisSize: MainAxisSize.min,
327 + mainAxisAlignment: MainAxisAlignment.start,
328 + children: <Widget>[
329 + currencyIcon,
330 + Expanded(
331 + child: Padding(
332 + padding: EdgeInsets.only(left: 12),
333 + child: Text(
334 + contact.name,
335 + style: TextStyle(
336 + fontSize: 14,
337 + fontWeight: FontWeight.normal,
338 + color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
339 + ),
340 + ),
341 + ))
342 + ],
343 + ),
344 + ),
345 + StandardListSeparator()
346 + ],
347 + );
348 + }
349
350 ActionPane _actionPane(BuildContext context, ContactRecord contact) => ActionPane(
351 motion: const ScrollMotion(),
@@ -208,7 +364,7 @@ class ContactListPage extends BasePage {
364 final isDelete = await showAlertDialog(context);
365
366 if (isDelete) {
211 - await contactListViewModel.delete(contact);
367 + await widget.contactListViewModel.delete(contact);
368 }
369 },
370 backgroundColor: Colors.red,
@@ -218,4 +374,80 @@ class ContactListPage extends BasePage {
374 ),
375 ],
376 );
377 +
378 + Widget filterButtonWidget(BuildContext context, ContactListViewModel contactListViewModel) {
379 + final filterIcon = Image.asset('assets/images/filter_icon.png',
380 + color: Theme.of(context).appBarTheme.titleTextStyle!.color);
381 + return MergeSemantics(
382 + child: SizedBox(
383 + height: 58,
384 + width: 58,
385 + child: ButtonTheme(
386 + minWidth: double.minPositive,
387 + child: Semantics(
388 + container: true,
389 + child: GestureDetector(
390 + onTap: () async {
391 + await showPopUp<void>(
392 + context: context,
393 + builder: (context) => FilterListWidget(
394 + initalType: contactListViewModel.orderType,
395 + initalAscending: contactListViewModel.ascending,
396 + onClose: (bool ascending, FilterListOrderType type) async {
397 + contactListViewModel.setAscending(ascending);
398 + await contactListViewModel.setOrderType(type);
399 + },
400 + ),
401 + );
402 + },
403 + child: Semantics(
404 + label: 'Transaction Filter',
405 + button: true,
406 + enabled: true,
407 + child: Container(
408 + height: 36,
409 + width: 36,
410 + decoration: BoxDecoration(
411 + shape: BoxShape.circle,
412 + color: Theme.of(context).extension<ExchangePageTheme>()!.buttonBackgroundColor,
413 + ),
414 + child: filterIcon,
415 + ),
416 + ),
417 + ),
418 + ),
419 + ),
420 + ),
421 + );
422 + }
423 +
424 + Future<bool> showAlertDialog(BuildContext context) async {
425 + return await showPopUp<bool>(
426 + context: context,
427 + builder: (BuildContext context) {
428 + return AlertWithTwoActions(
429 + alertTitle: S.of(context).address_remove_contact,
430 + alertContent: S.of(context).address_remove_content,
431 + rightButtonText: S.of(context).remove,
432 + leftButtonText: S.of(context).cancel,
433 + actionRightButton: () => Navigator.of(context).pop(true),
434 + actionLeftButton: () => Navigator.of(context).pop(false));
435 + }) ??
436 + false;
437 + }
438 +
439 + Future<bool> showNameAndAddressDialog(BuildContext context, String name, String address) async {
440 + return await showPopUp<bool>(
441 + context: context,
442 + builder: (BuildContext context) {
443 + return AlertWithTwoActions(
444 + alertTitle: name,
445 + alertContent: address,
446 + rightButtonText: S.of(context).copy,
447 + leftButtonText: S.of(context).cancel,
448 + actionRightButton: () => Navigator.of(context).pop(true),
449 + actionLeftButton: () => Navigator.of(context).pop(false));
450 + }) ??
451 + false;
452 + }
453 }
lib/src/screens/dashboard/widgets/filter_list_widget.dart
+13 -13
@@ -18,9 +18,9 @@ class FilterListWidget extends StatefulWidget {
18 required this.onClose,
19 });
20
21 - final WalletListOrderType? initalType;
21 + final FilterListOrderType? initalType;
22 final bool initalAscending;
23 - final Function(bool, WalletListOrderType) onClose;
23 + final Function(bool, FilterListOrderType) onClose;
24
25 @override
26 FilterListWidgetState createState() => FilterListWidgetState();
@@ -28,7 +28,7 @@ class FilterListWidget extends StatefulWidget {
28
29 class FilterListWidgetState extends State<FilterListWidget> {
30 late bool ascending;
31 - late WalletListOrderType? type;
31 + late FilterListOrderType? type;
32
33 @override
34 void initState() {
@@ -37,7 +37,7 @@ class FilterListWidgetState extends State<FilterListWidget> {
37 type = widget.initalType;
38 }
39
40 - void setSelectedOrderType(WalletListOrderType? orderType) {
40 + void setSelectedOrderType(FilterListOrderType? orderType) {
41 setState(() {
42 type = orderType;
43 });
@@ -72,7 +72,7 @@ class FilterListWidgetState extends State<FilterListWidget> {
72 ),
73 ),
74 ),
75 - if (type != WalletListOrderType.Custom) ...[
75 + if (type != FilterListOrderType.Custom) ...[
76 sectionDivider,
77 SettingsChoicesCell(
78 ChoicesListItem<ListOrderMode>(
@@ -89,10 +89,10 @@ class FilterListWidgetState extends State<FilterListWidget> {
89 ],
90 sectionDivider,
91 RadioListTile(
92 - value: WalletListOrderType.CreationDate,
92 + value: FilterListOrderType.CreationDate,
93 groupValue: type,
94 title: Text(
95 - WalletListOrderType.CreationDate.toString(),
95 + FilterListOrderType.CreationDate.toString(),
96 style: TextStyle(
97 color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
98 fontSize: 16,
@@ -104,10 +104,10 @@ class FilterListWidgetState extends State<FilterListWidget> {
104 activeColor: Theme.of(context).primaryColor,
105 ),
106 RadioListTile(
107 - value: WalletListOrderType.Alphabetical,
107 + value: FilterListOrderType.Alphabetical,
108 groupValue: type,
109 title: Text(
110 - WalletListOrderType.Alphabetical.toString(),
110 + FilterListOrderType.Alphabetical.toString(),
111 style: TextStyle(
112 color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
113 fontSize: 16,
@@ -119,10 +119,10 @@ class FilterListWidgetState extends State<FilterListWidget> {
119 activeColor: Theme.of(context).primaryColor,
120 ),
121 RadioListTile(
122 - value: WalletListOrderType.GroupByType,
122 + value: FilterListOrderType.GroupByType,
123 groupValue: type,
124 title: Text(
125 - WalletListOrderType.GroupByType.toString(),
125 + FilterListOrderType.GroupByType.toString(),
126 style: TextStyle(
127 color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
128 fontSize: 16,
@@ -134,10 +134,10 @@ class FilterListWidgetState extends State<FilterListWidget> {
134 activeColor: Theme.of(context).primaryColor,
135 ),
136 RadioListTile(
137 - value: WalletListOrderType.Custom,
137 + value: FilterListOrderType.Custom,
138 groupValue: type,
139 title: Text(
140 - WalletListOrderType.Custom.toString(),
140 + FilterListOrderType.Custom.toString(),
141 style: TextStyle(
142 color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
143 fontSize: 16,
lib/src/screens/wallet_list/filtered_list.dart
+30 -16
@@ -7,13 +7,17 @@ class FilteredList extends StatefulWidget {
7 required this.list,
8 required this.itemBuilder,
9 required this.updateFunction,
10 + this.canReorder = true,
11 this.shrinkWrap = false,
12 + this.physics,
13 });
14
15 final ObservableList<dynamic> list;
16 final Widget Function(BuildContext, int) itemBuilder;
17 final Function updateFunction;
18 + final bool canReorder;
19 final bool shrinkWrap;
20 + final ScrollPhysics? physics;
21
22 @override
23 FilteredListState createState() => FilteredListState();
@@ -22,21 +26,31 @@ class FilteredList extends StatefulWidget {
26 class FilteredListState extends State<FilteredList> {
27 @override
28 Widget build(BuildContext context) {
25 - return Observer(
26 - builder: (_) => ReorderableListView.builder(
27 - shrinkWrap: widget.shrinkWrap,
28 - physics: const BouncingScrollPhysics(),
29 - itemBuilder: widget.itemBuilder,
30 - itemCount: widget.list.length,
31 - onReorder: (int oldIndex, int newIndex) {
32 - if (oldIndex < newIndex) {
33 - newIndex -= 1;
34 - }
35 - final dynamic item = widget.list.removeAt(oldIndex);
36 - widget.list.insert(newIndex, item);
37 - widget.updateFunction();
38 - },
39 - ),
40 - );
29 + if (widget.canReorder) {
30 + return Observer(
31 + builder: (_) => ReorderableListView.builder(
32 + shrinkWrap: widget.shrinkWrap,
33 + physics: widget.physics ?? const BouncingScrollPhysics(),
34 + itemBuilder: widget.itemBuilder,
35 + itemCount: widget.list.length,
36 + onReorder: (int oldIndex, int newIndex) {
37 + if (oldIndex < newIndex) {
38 + newIndex -= 1;
39 + }
40 + final dynamic item = widget.list.removeAt(oldIndex);
41 + widget.list.insert(newIndex, item);
42 + widget.updateFunction();
43 + },
44 + ),
45 + );
46 + } else {
47 + return Observer(
48 + builder: (_) => ListView.builder(
49 + physics: widget.physics ?? const BouncingScrollPhysics(),
50 + itemBuilder: widget.itemBuilder,
51 + itemCount: widget.list.length,
52 + ),
53 + );
54 + }
55 }
56 }
lib/src/screens/wallet_list/wallet_list_page.dart
+1 -1
@@ -59,7 +59,7 @@ class WalletListPage extends BasePage {
59 builder: (context) => FilterListWidget(
60 initalType: walletListViewModel.orderType,
61 initalAscending: walletListViewModel.ascending,
62 - onClose: (bool ascending, WalletListOrderType type) async {
62 + onClose: (bool ascending, FilterListOrderType type) async {
63 walletListViewModel.setAscending(ascending);
64 await walletListViewModel.setOrderType(type);
65 },
lib/src/widgets/collapsible_standart_list.dart deleted
-39
@@ -1,39 +0,0 @@
1 -import 'package:cake_wallet/src/widgets/standard_list.dart';
2 -import 'package:flutter/material.dart';
3 -
4 -class CollapsibleSectionList extends SectionStandardList {
5 - CollapsibleSectionList(
6 - {required int sectionCount,
7 - required int Function(int sectionIndex) itemCounter,
8 - required Widget Function(int sectionIndex, int itemIndex) itemBuilder,
9 - Widget Function(int sectionIndex)? sectionTitleBuilder,
10 - bool hasTopSeparator = false})
11 - : super(
12 - hasTopSeparator: hasTopSeparator,
13 - sectionCount: sectionCount,
14 - itemCounter: itemCounter,
15 - itemBuilder: itemBuilder,
16 - sectionTitleBuilder: sectionTitleBuilder);
17 -
18 - @override
19 - Widget buildTitle(List<Widget> items, int sectionIndex) {
20 - if (sectionTitleBuilder == null) {
21 - throw Exception('Cannot to build title. sectionTitleBuilder is null');
22 - }
23 - return sectionTitleBuilder!.call(sectionIndex);
24 - }
25 -
26 - @override
27 - List<Widget> buildSection(int itemCount, List<Widget> items, int sectionIndex) {
28 - final List<Widget> section = [];
29 -
30 - for (var itemIndex = 0; itemIndex < itemCount; itemIndex++) {
31 - final item = itemBuilder(sectionIndex, itemIndex);
32 -
33 - section.add(StandardListSeparator());
34 -
35 - section.add(item);
36 - }
37 - return section;
38 - }
39 -}
lib/store/settings_store.dart
+34 -6
@@ -62,9 +62,11 @@ abstract class SettingsStoreBase with Store {
62 required bool initialAppSecure,
63 required bool initialDisableBuy,
64 required bool initialDisableSell,
65 + required FilterListOrderType initialWalletListOrder,
66 + required FilterListOrderType initialContactListOrder,
67 required bool initialDisableBulletin,
66 - required WalletListOrderType initialWalletListOrder,
68 required bool initialWalletListAscending,
69 + required bool initialContactListAscending,
70 required FiatApiMode initialFiatMode,
71 required bool initialAllowBiometricalAuthentication,
72 required String initialTotpSecretKey,
@@ -149,7 +151,9 @@ abstract class SettingsStoreBase with Store {
151 disableSell = initialDisableSell,
152 disableBulletin = initialDisableBulletin,
153 walletListOrder = initialWalletListOrder,
154 + contactListOrder = initialContactListOrder,
155 walletListAscending = initialWalletListAscending,
156 + contactListAscending = initialContactListAscending,
157 shouldShowMarketPlaceInDashboard = initialShouldShowMarketPlaceInDashboard,
158 exchangeStatus = initialExchangeStatus,
159 currentTheme = initialTheme,
@@ -324,14 +328,24 @@ abstract class SettingsStoreBase with Store {
328
329 reaction(
330 (_) => walletListOrder,
327 - (WalletListOrderType walletListOrder) =>
331 + (FilterListOrderType walletListOrder) =>
332 sharedPreferences.setInt(PreferencesKey.walletListOrder, walletListOrder.index));
333
334 + reaction(
335 + (_) => contactListOrder,
336 + (FilterListOrderType contactListOrder) =>
337 + sharedPreferences.setInt(PreferencesKey.contactListOrder, contactListOrder.index));
338 +
339 reaction(
340 (_) => walletListAscending,
341 (bool walletListAscending) =>
342 sharedPreferences.setBool(PreferencesKey.walletListAscending, walletListAscending));
343
344 + reaction(
345 + (_) => contactListAscending,
346 + (bool contactListAscending) =>
347 + sharedPreferences.setBool(PreferencesKey.contactListAscending, contactListAscending));
348 +
349 reaction(
350 (_) => autoGenerateSubaddressStatus,
351 (AutoGenerateSubaddressStatus autoGenerateSubaddressStatus) => sharedPreferences.setInt(
@@ -645,15 +659,21 @@ abstract class SettingsStoreBase with Store {
659 @observable
660 bool disableSell;
661
662 + @observable
663 + FilterListOrderType contactListOrder;
664 +
665 @observable
666 bool disableBulletin;
667
668 @observable
652 - WalletListOrderType walletListOrder;
669 + FilterListOrderType walletListOrder;
670
671 @observable
672 bool walletListAscending;
673
674 + @observable
675 + bool contactListAscending;
676 +
677 @observable
678 bool allowBiometricalAuthentication;
679
@@ -907,9 +927,13 @@ abstract class SettingsStoreBase with Store {
927 final disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? false;
928 final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false;
929 final walletListOrder =
910 - WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
930 + FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
931 + final contactListOrder =
932 + FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.contactListOrder) ?? 0];
933 final walletListAscending =
934 sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true;
935 + final contactListAscending =
936 + sharedPreferences.getBool(PreferencesKey.contactListAscending) ?? true;
937 final currentFiatApiMode = FiatApiMode.deserialize(
938 raw: sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey) ??
939 FiatApiMode.enabled.raw);
@@ -1200,6 +1224,8 @@ abstract class SettingsStoreBase with Store {
1224 initialDisableBulletin: disableBulletin,
1225 initialWalletListOrder: walletListOrder,
1226 initialWalletListAscending: walletListAscending,
1227 + initialContactListOrder: contactListOrder,
1228 + initialContactListAscending: contactListAscending,
1229 initialFiatMode: currentFiatApiMode,
1230 initialAllowBiometricalAuthentication: allowBiometricalAuthentication,
1231 initialCake2FAPresetOptions: selectedCake2FAPreset,
@@ -1348,9 +1374,11 @@ abstract class SettingsStoreBase with Store {
1374 disableBulletin =
1375 sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin;
1376 walletListOrder =
1351 - WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
1377 + FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
1378 + contactListOrder =
1379 + FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.contactListOrder) ?? 0];
1380 walletListAscending = sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true;
1353 -
1381 + contactListAscending = sharedPreferences.getBool(PreferencesKey.contactListAscending) ?? true;
1382 shouldShowMarketPlaceInDashboard =
1383 sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ??
1384 shouldShowMarketPlaceInDashboard;
lib/view_model/contact_list/contact_list_view_model.dart
+79 -5
@@ -1,18 +1,20 @@
1 import 'dart:async';
2 +
3 import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
4 +import 'package:cake_wallet/entities/contact.dart';
5 import 'package:cake_wallet/entities/contact_base.dart';
6 +import 'package:cake_wallet/entities/contact_record.dart';
7 import 'package:cake_wallet/entities/wallet_contact.dart';
8 +import 'package:cake_wallet/entities/wallet_list_order_types.dart';
9 import 'package:cake_wallet/generated/i18n.dart';
10 import 'package:cake_wallet/store/settings_store.dart';
11 +import 'package:cake_wallet/utils/mobx.dart';
12 +import 'package:collection/collection.dart';
13 +import 'package:cw_core/crypto_currency.dart';
14 import 'package:cw_core/wallet_info.dart';
15 import 'package:cw_core/wallet_type.dart';
16 import 'package:hive/hive.dart';
17 import 'package:mobx/mobx.dart';
11 -import 'package:cake_wallet/entities/contact_record.dart';
12 -import 'package:cake_wallet/entities/contact.dart';
13 -import 'package:cake_wallet/utils/mobx.dart';
14 -import 'package:cw_core/crypto_currency.dart';
15 -import 'package:collection/collection.dart';
18
19 part 'contact_list_view_model.g.dart';
20
@@ -75,6 +77,8 @@ abstract class ContactListViewModelBase with Store {
77 _subscription = contactSource.bindToListWithTransform(
78 contacts, (Contact contact) => ContactRecord(contactSource, contact),
79 initialFire: true);
80 +
81 + setOrderType(settingsStore.contactListOrder);
82 }
83
84 String _createName(String walletName, String label, {int? key = null}) {
@@ -93,6 +97,10 @@ abstract class ContactListViewModelBase with Store {
97
98 bool get isEditable => _currency == null;
99
100 + FilterListOrderType? get orderType => settingsStore.contactListOrder;
101 +
102 + bool get ascending => settingsStore.contactListAscending;
103 +
104 @computed
105 bool get shouldRequireTOTP2FAForAddingContacts =>
106 settingsStore.shouldRequireTOTP2FAForAddingContacts;
@@ -118,4 +126,70 @@ abstract class ContactListViewModelBase with Store {
126 _currency?.toString() == element.type.tag ||
127 _currency?.tag == element.type.toString();
128 }
129 +
130 + void dispose() async {
131 + _subscription?.cancel();
132 + final List<Contact> contactsSourceCopy = contacts.map((e) => e.original).toList();
133 + await reorderContacts(contactsSourceCopy);
134 + }
135 +
136 + void reorderAccordingToContactList() =>
137 + settingsStore.contactListOrder = FilterListOrderType.Custom;
138 +
139 + Future<void> reorderContacts(List<Contact> contactCopy) async {
140 + await contactSource.deleteAll(contactCopy.map((e) => e.key).toList());
141 + await contactSource.addAll(contactCopy);
142 + }
143 +
144 + Future<void> sortGroupByType() async {
145 + List<Contact> contactsSourceCopy = contactSource.values.toList();
146 +
147 + contactsSourceCopy.sort((a, b) => ascending
148 + ? a.type.toString().compareTo(b.type.toString())
149 + : b.type.toString().compareTo(a.type.toString()));
150 +
151 + await reorderContacts(contactsSourceCopy);
152 + }
153 +
154 + Future<void> sortAlphabetically() async {
155 + List<Contact> contactsSourceCopy = contactSource.values.toList();
156 +
157 + contactsSourceCopy
158 + .sort((a, b) => ascending ? a.name.compareTo(b.name) : b.name.compareTo(a.name));
159 +
160 + await reorderContacts(contactsSourceCopy);
161 + }
162 +
163 + Future<void> sortByCreationDate() async {
164 + List<Contact> contactsSourceCopy = contactSource.values.toList();
165 +
166 + contactsSourceCopy.sort((a, b) =>
167 + ascending ? a.lastChange.compareTo(b.lastChange) : b.lastChange.compareTo(a.lastChange));
168 +
169 + await reorderContacts(contactsSourceCopy);
170 + }
171 +
172 + void setAscending(bool ascending) => settingsStore.contactListAscending = ascending;
173 +
174 + Future<void> setOrderType(FilterListOrderType? type) async {
175 + if (type == null) return;
176 +
177 + settingsStore.contactListOrder = type;
178 +
179 + switch (type) {
180 + case FilterListOrderType.CreationDate:
181 + await sortByCreationDate();
182 + break;
183 + case FilterListOrderType.Alphabetical:
184 + await sortAlphabetically();
185 + break;
186 + case FilterListOrderType.GroupByType:
187 + await sortGroupByType();
188 + break;
189 + case FilterListOrderType.Custom:
190 + default:
191 + reorderAccordingToContactList();
192 + break;
193 + }
194 + }
195 }
lib/view_model/contact_list/contact_view_model.dart
+20 -4
@@ -2,7 +2,7 @@ import 'package:cake_wallet/entities/contact_record.dart';
2 import 'package:hive/hive.dart';
3 import 'package:mobx/mobx.dart';
4 import 'package:cake_wallet/core/execution_state.dart';
5 -import 'package:cw_core/wallet_base.dart';
5 +import 'package:cake_wallet/generated/i18n.dart';
6 import 'package:cake_wallet/entities/contact.dart';
7 import 'package:cw_core/crypto_currency.dart';
8
@@ -17,7 +17,9 @@ abstract class ContactViewModelBase with Store {
17 _contact = contact,
18 name = contact?.name ?? '',
19 address = contact?.address ?? '',
20 - currency = contact?.type;
20 + currency = contact?.type,
21 + lastChange = contact?.lastChange;
22 +
23
24 @observable
25 ExecutionState state;
@@ -31,6 +33,8 @@ abstract class ContactViewModelBase with Store {
33 @observable
34 CryptoCurrency? currency;
35
36 + DateTime? lastChange;
37 +
38 @computed
39 bool get isReady =>
40 name.isNotEmpty &&
@@ -51,20 +55,32 @@ abstract class ContactViewModelBase with Store {
55 Future<void> save() async {
56 try {
57 state = IsExecutingState();
58 + final now = DateTime.now();
59 +
60 + if (doesContactNameExist(name)) {
61 + state = FailureState(S.current.contact_name_exists);
62 + return;
63 + }
64
65 if (_contact != null && _contact!.original.isInBox) {
66 _contact?.name = name;
67 _contact?.address = address;
68 _contact?.type = currency!;
69 + _contact?.lastChange = now;
70 await _contact?.save();
71 } else {
72 await _contacts
62 - .add(Contact(name: name, address: address, type: currency!));
73 + .add(Contact(name: name, address: address, type: currency!, lastChange: now));
74 }
75
76 + lastChange = now;
77 state = ExecutedSuccessfullyState();
78 } catch (e) {
79 state = FailureState(e.toString());
80 }
81 }
70 -}
82 +
83 + bool doesContactNameExist(String name) {
84 + return _contacts.values.any((contact) => contact.name == name);
85 + }
86 +}
\ No newline at end of file
lib/view_model/wallet_list/wallet_list_view_model.dart
+7 -7
@@ -76,7 +76,7 @@ abstract class WalletListViewModelBase with Store {
76 await _appStore.changeCurrentWallet(wallet);
77 }
78
79 - WalletListOrderType? get orderType => _appStore.settingsStore.walletListOrder;
79 + FilterListOrderType? get orderType => _appStore.settingsStore.walletListOrder;
80
81 bool get ascending => _appStore.settingsStore.walletListAscending;
82
@@ -108,7 +108,7 @@ abstract class WalletListViewModelBase with Store {
108 return;
109 }
110
111 - _appStore.settingsStore.walletListOrder = WalletListOrderType.Custom;
111 + _appStore.settingsStore.walletListOrder = FilterListOrderType.Custom;
112
113 // make a copy of the walletInfoSource:
114 List<WalletInfo> walletInfoSourceCopy = _walletInfoSource.values.toList();
@@ -186,22 +186,22 @@ abstract class WalletListViewModelBase with Store {
186 _appStore.settingsStore.walletListAscending = ascending;
187 }
188
189 - Future<void> setOrderType(WalletListOrderType? type) async {
189 + Future<void> setOrderType(FilterListOrderType? type) async {
190 if (type == null) return;
191
192 _appStore.settingsStore.walletListOrder = type;
193
194 switch (type) {
195 - case WalletListOrderType.CreationDate:
195 + case FilterListOrderType.CreationDate:
196 await sortByCreationDate();
197 break;
198 - case WalletListOrderType.Alphabetical:
198 + case FilterListOrderType.Alphabetical:
199 await sortAlphabetically();
200 break;
201 - case WalletListOrderType.GroupByType:
201 + case FilterListOrderType.GroupByType:
202 await sortGroupByType();
203 break;
204 - case WalletListOrderType.Custom:
204 + case FilterListOrderType.Custom:
205 default:
206 await reorderAccordingToWalletList();
207 break;
res/values/strings_ar.arb
+3 -2
@@ -937,5 +937,6 @@
937 "you_pay": "انت تدفع",
938 "you_will_get": "حول الى",
939 "you_will_send": "تحويل من",
940 - "yy": "YY"
941 -}
\ No newline at end of file
940 + "yy": "YY",
941 + "contact_name_exists": " .ﻒﻠﺘﺨﻣ ﻢﺳﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ .ﻞﻌﻔﻟﺎﺑ ﺓﺩﻮﺟﻮﻣ ﻢﺳﻻﺍ ﺍﺬﻬﺑ ﻝﺎﺼﺗﺍ ﺔﻬﺟ"
942 +}
res/values/strings_bg.arb
+3 -2
@@ -937,5 +937,6 @@
937 "you_pay": "Вие плащате",
938 "you_will_get": "Обръщане в",
939 "you_will_send": "Обръщане от",
940 - "yy": "гг"
941 -}
\ No newline at end of file
940 + "yy": "гг",
941 + "contact_name_exists": "Вече съществува контакт с това име. Моля, изберете друго име."
942 +}
res/values/strings_cs.arb
+3 -2
@@ -937,5 +937,6 @@
937 "you_pay": "Zaplatíte",
938 "you_will_get": "Směnit na",
939 "you_will_send": "Směnit z",
940 - "yy": "YY"
941 -}
\ No newline at end of file
940 + "yy": "YY",
941 + "contact_name_exists": "Kontakt s tímto jménem již existuje. Vyberte prosím jiný název."
942 +}
res/values/strings_en.arb
+3 -2
@@ -940,5 +940,6 @@
940 "you_pay": "You Pay",
941 "you_will_get": "Convert to",
942 "you_will_send": "Convert from",
943 - "yy": "YY"
944 -}
\ No newline at end of file
943 + "yy": "YY",
944 + "contact_name_exists": "A contact with that name already exists. Please choose a different name."
945 +}
res/values/strings_es.arb
+2 -1
@@ -938,5 +938,6 @@
938 "you_pay": "Tú pagas",
939 "you_will_get": "Convertir a",
940 "you_will_send": "Convertir de",
941 - "yy": "YY"
941 + "yy": "YY",
942 + "contact_name_exists": "Ya existe un contacto con ese nombre. Elija un nombre diferente."
943 }
res/values/strings_fr.arb
+3 -2
@@ -937,5 +937,6 @@
937 "you_pay": "Vous payez",
938 "you_will_get": "Convertir vers",
939 "you_will_send": "Convertir depuis",
940 - "yy": "AA"
941 -}
\ No newline at end of file
940 + "yy": "AA",
941 + "contact_name_exists": "Un contact portant ce nom existe déjà. Veuillez choisir un autre nom."
942 +}
res/values/strings_ha.arb
+3 -2
@@ -939,5 +939,6 @@
939 "you_pay": "Ka Bayar",
940 "you_will_get": "Maida zuwa",
941 "you_will_send": "Maida daga",
942 - "yy": "YY"
943 -}
\ No newline at end of file
942 + "yy": "YY",
943 + "contact_name_exists": "An riga an sami lamba tare da wannan sunan. Da fatan za a zaɓi suna daban."
944 +}
res/values/strings_hi.arb
+3 -2
@@ -939,5 +939,6 @@
939 "you_pay": "आप भुगतान करते हैं",
940 "you_will_get": "में बदलें",
941 "you_will_send": "से रूपांतरित करें",
942 - "yy": "वाईवाई"
943 -}
\ No newline at end of file
942 + "yy": "वाईवाई",
943 + "contact_name_exists": "उस नाम का एक संपर्क पहले से मौजूद है. कृपया कोई भिन्न नाम चुनें."
944 +}
res/values/strings_hr.arb
+3 -2
@@ -937,5 +937,6 @@
937 "you_pay": "Vi plaćate",
938 "you_will_get": "Razmijeni u",
939 "you_will_send": "Razmijeni iz",
940 - "yy": "GG"
941 -}
\ No newline at end of file
940 + "yy": "GG",
941 + "contact_name_exists": "Kontakt s tim imenom već postoji. Odaberite drugo ime."
942 +}
res/values/strings_id.arb
+3 -2
@@ -940,5 +940,6 @@
940 "you_pay": "Anda Membayar",
941 "you_will_get": "Konversi ke",
942 "you_will_send": "Konversi dari",
943 - "yy": "YY"
944 -}
\ No newline at end of file
943 + "yy": "YY",
944 + "contact_name_exists": "Kontak dengan nama tersebut sudah ada. Silakan pilih nama lain."
945 +}
res/values/strings_it.arb
+3 -2
@@ -940,5 +940,6 @@
940 "you_pay": "Tu paghi",
941 "you_will_get": "Converti a",
942 "you_will_send": "Conveti da",
943 - "yy": "YY"
944 -}
\ No newline at end of file
943 + "yy": "YY",
944 + "contact_name_exists": "Esiste già un contatto con quel nome. Scegli un nome diverso."
945 +}
res/values/strings_ja.arb
+3 -2
@@ -938,5 +938,6 @@
938 "you_pay": "あなたが支払う",
939 "you_will_get": "に変換",
940 "you_will_send": "から変換",
941 - "yy": "YY"
942 -}
\ No newline at end of file
941 + "yy": "YY",
942 + "contact_name_exists": "その名前の連絡先はすでに存在します。別の名前を選択してください。"
943 +}
res/values/strings_ko.arb
+3 -2
@@ -939,5 +939,6 @@
939 "you_will_get": "로 변환하다",
940 "you_will_send": "다음에서 변환",
941 "YY": "YY",
942 - "yy": "YY"
943 -}
\ No newline at end of file
942 + "yy": "YY",
943 + "contact_name_exists": "해당 이름을 가진 연락처가 이미 존재합니다. 다른 이름을 선택하세요."
944 +}
res/values/strings_my.arb
+3 -2
@@ -937,5 +937,6 @@
937 "you_pay": "သင်ပေးချေပါ။",
938 "you_will_get": "သို့ပြောင်းပါ။",
939 "you_will_send": "မှပြောင်းပါ။",
940 - "yy": "YY"
941 -}
\ No newline at end of file
940 + "yy": "YY",
941 + "contact_name_exists": "ထိုအမည်နှင့် အဆက်အသွယ်တစ်ခု ရှိနှင့်ပြီးဖြစ်သည်။ အခြားအမည်တစ်ခုကို ရွေးပါ။"
942 +}
res/values/strings_nl.arb
+3 -2
@@ -938,5 +938,6 @@
938 "you_pay": "U betaalt",
939 "you_will_get": "Converteren naar",
940 "you_will_send": "Converteren van",
941 - "yy": "JJ"
942 -}
\ No newline at end of file
941 + "yy": "JJ",
942 + "contact_name_exists": "Er bestaat al een contact met die naam. Kies een andere naam."
943 +}
res/values/strings_pl.arb
+3 -2
@@ -937,5 +937,6 @@
937 "you_pay": "Płacisz",
938 "you_will_get": "Konwertuj na",
939 "you_will_send": "Konwertuj z",
940 - "yy": "RR"
941 -}
\ No newline at end of file
940 + "yy": "RR",
941 + "contact_name_exists": "Kontakt o tej nazwie już istnieje. Proszę wybrać inną nazwę."
942 +}
res/values/strings_pt.arb
+1 -1
@@ -941,4 +941,4 @@
941 "you_will_get": "Converter para",
942 "you_will_send": "Converter de",
943 "yy": "aa"
944 -}
\ No newline at end of file
944 +}
res/values/strings_ru.arb
+3 -2
@@ -938,5 +938,6 @@
938 "you_pay": "Вы платите",
939 "you_will_get": "Конвертировать в",
940 "you_will_send": "Конвертировать из",
941 - "yy": "ГГ"
942 -}
\ No newline at end of file
941 + "yy": "ГГ",
942 + "contact_name_exists": "Контакт с таким именем уже существует. Пожалуйста, выберите другое имя."
943 +}
res/values/strings_th.arb
+3 -2
@@ -937,5 +937,6 @@
937 "you_pay": "คุณจ่าย",
938 "you_will_get": "แปลงเป็น",
939 "you_will_send": "แปลงจาก",
940 - "yy": "ปี"
941 -}
\ No newline at end of file
940 + "yy": "ปี",
941 + "contact_name_exists": "มีผู้ติดต่อชื่อนั้นอยู่แล้ว โปรดเลือกชื่ออื่น"
942 +}
res/values/strings_tl.arb
+1 -1
@@ -938,4 +938,4 @@
938 "you_will_get": "I-convert sa",
939 "you_will_send": "I-convert mula sa",
940 "yy": "YY"
941 -}
\ No newline at end of file
941 +}
res/values/strings_tr.arb
+3 -2
@@ -937,5 +937,6 @@
937 "you_pay": "Şu kadar ödeyeceksin: ",
938 "you_will_get": "Biçimine dönüştür:",
939 "you_will_send": "Biçiminden dönüştür:",
940 - "yy": "YY"
941 -}
\ No newline at end of file
940 + "yy": "YY",
941 + "contact_name_exists": "Bu isimde bir kişi zaten mevcut. Lütfen farklı bir ad seçin."
942 +}
res/values/strings_uk.arb
+3 -2
@@ -938,5 +938,6 @@
938 "you_pay": "Ви платите",
939 "you_will_get": "Конвертувати в",
940 "you_will_send": "Конвертувати з",
941 - "yy": "YY"
942 -}
\ No newline at end of file
941 + "yy": "YY",
942 + "contact_name_exists": "Контакт із такою назвою вже існує. Виберіть інше ім'я."
943 +}
res/values/strings_ur.arb
+3 -2
@@ -939,5 +939,6 @@
939 "you_pay": "تم ادا کرو",
940 "you_will_get": "میں تبدیل کریں۔",
941 "you_will_send": "سے تبدیل کریں۔",
942 - "yy": "YY"
943 -}
\ No newline at end of file
942 + "yy": "YY",
943 + "contact_name_exists": " ۔ﮟﯾﺮﮐ ﺐﺨﺘﻨﻣ ﻡﺎﻧ ﻒﻠﺘﺨﻣ ﮏﯾﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔ﮯﮨ ﺩﻮﺟﻮﻣ ﮯﺳ ﮯﻠﮩﭘ ﮧﻄﺑﺍﺭ ﮏﯾﺍ ﮫﺗﺎﺳ ﮯﮐ ﻡﺎﻧ ﺱﺍ"
944 +}
res/values/strings_yo.arb
+3 -2
@@ -938,5 +938,6 @@
938 "you_pay": "Ẹ sàn",
939 "you_will_get": "Ṣe pàṣípààrọ̀ sí",
940 "you_will_send": "Ṣe pàṣípààrọ̀ láti",
941 - "yy": "Ọd"
942 -}
\ No newline at end of file
941 + "yy": "Ọd",
942 + "contact_name_exists": "Olubasọrọ pẹlu orukọ yẹn ti wa tẹlẹ. Jọwọ yan orukọ ti o yatọ."
943 +}
res/values/strings_zh.arb
+3 -2
@@ -937,5 +937,6 @@
937 "you_pay": "你付钱",
938 "you_will_get": "转换到",
939 "you_will_send": "转换自",
940 - "yy": "YY"
941 -}
\ No newline at end of file
940 + "yy": "YY",
941 + "contact_name_exists": "已存在具有该名称的联系人。请选择不同的名称。"
942 +}