Cw 744 improve address book (#1771)
* 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 * fix the address book filter by the selected currency * add dropdown for wallets with multiple address types * minor fixes * add dropdown for wallets with multiple address types * Update lib/entities/contact.dart [skip ci] * Update lib/src/screens/contact/contact_list_page.dart [skip ci] * Update lib/src/screens/contact/contact_list_page.dart [skip ci] --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
Serhii committed
Nov 12, 2024 at 05:29 UTC
96db38c0aa3b2628118302e137e7b1969b5367d0
2 files changed
+75
-22
lib/src/screens/contact/contact_list_page.dart
+53
-9
@@ -11,6 +11,7 @@ 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/themes/extensions/filter_theme.dart';
15
import 'package:cake_wallet/utils/show_bar.dart';
16
import 'package:cake_wallet/utils/show_pop_up.dart';
17
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
@@ -160,25 +161,60 @@ class _ContactPageBodyState extends State<ContactPageBody> with SingleTickerProv
161
Widget _buildWalletContacts(BuildContext context) {
162
final walletContacts = widget.contactListViewModel.walletContactsToShow;
163
164
+ final groupedContacts = <String, List<ContactBase>>{};
165
+ for (var contact in walletContacts) {
166
+ final baseName = _extractBaseName(contact.name);
167
+ groupedContacts.putIfAbsent(baseName, () => []).add(contact);
168
+ }
169
+
170
return ListView.builder(
164
- shrinkWrap: true,
165
- itemCount: walletContacts.length * 2,
171
+ itemCount: groupedContacts.length * 2,
172
itemBuilder: (context, index) {
173
if (index.isOdd) {
174
return StandardListSeparator();
175
} else {
170
- final walletInfo = walletContacts[index ~/ 2];
171
- return generateRaw(context, walletInfo);
176
+ final groupIndex = index ~/ 2;
177
+ final groupName = groupedContacts.keys.elementAt(groupIndex);
178
+ final groupContacts = groupedContacts[groupName]!;
179
+
180
+ if (groupContacts.length == 1) {
181
+ final contact = groupContacts[0];
182
+ return generateRaw(context, contact);
183
+ } else {
184
+ final activeContact = groupContacts.firstWhere(
185
+ (contact) => contact.name.contains('Active'),
186
+ orElse: () => groupContacts[0],
187
+ );
188
+
189
+ return ExpansionTile(
190
+ title: Text(
191
+ groupName,
192
+ style: TextStyle(
193
+ fontSize: 14,
194
+ fontWeight: FontWeight.normal,
195
+ color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
196
+ ),
197
+ ),
198
+ leading: _buildCurrencyIcon(activeContact),
199
+ tilePadding: EdgeInsets.zero,
200
+ childrenPadding: const EdgeInsets.only(left: 16),
201
+ expandedCrossAxisAlignment: CrossAxisAlignment.start,
202
+ expandedAlignment: Alignment.topLeft,
203
+ children: groupContacts.map((contact) => generateRaw(context, contact)).toList(),
204
+ );
205
+ }
206
}
207
},
208
);
209
}
210
211
+ String _extractBaseName(String name) {
212
+ final bracketIndex = name.indexOf('(');
213
+ return (bracketIndex != -1) ? name.substring(0, bracketIndex).trim() : name;
214
+ }
215
+
216
Widget generateRaw(BuildContext context, ContactBase contact) {
178
- final image = contact.type.iconPath;
179
- final currencyIcon = image != null
180
- ? Image.asset(image, height: 24, width: 24)
181
- : const SizedBox(height: 24, width: 24);
217
+ final currencyIcon = _buildCurrencyIcon(contact);
218
219
return GestureDetector(
220
onTap: () async {
@@ -219,6 +255,13 @@ class _ContactPageBodyState extends State<ContactPageBody> with SingleTickerProv
255
);
256
}
257
258
+ Widget _buildCurrencyIcon(ContactBase contact) {
259
+ final image = contact.type.iconPath;
260
+ return image != null
261
+ ? Image.asset(image, height: 24, width: 24)
262
+ : const SizedBox(height: 24, width: 24);
263
+ }
264
+
265
Future<bool> showNameAndAddressDialog(BuildContext context, String name, String address) async {
266
return await showPopUp<bool>(
267
context: context,
@@ -263,12 +306,13 @@ class _ContactListBodyState extends State<ContactListBody> {
306
@override
307
void dispose() {
308
widget.tabController.removeListener(_handleTabChange);
309
+ widget.contactListViewModel.dispose();
310
super.dispose();
311
}
312
313
@override
314
Widget build(BuildContext context) {
271
- final contacts = widget.contactListViewModel.contacts;
315
+ final contacts = widget.contactListViewModel.contactsToShow;
316
return Scaffold(
317
body: Container(
318
child: FilteredList(
lib/view_model/contact_list/contact_list_view_model.dart
+22
-13
@@ -28,7 +28,8 @@ abstract class ContactListViewModelBase with Store {
28
isAutoGenerateEnabled =
29
settingsStore.autoGenerateSubaddressStatus == AutoGenerateSubaddressStatus.enabled {
30
walletInfoSource.values.forEach((info) {
31
- if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) && info.addressInfos != null) {
31
+ if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) &&
32
+ info.addressInfos != null) {
33
for (var key in info.addressInfos!.keys) {
34
final value = info.addressInfos![key];
35
final address = value?.first;
@@ -60,15 +61,19 @@ abstract class ContactListViewModelBase with Store {
61
address,
62
name,
63
walletTypeToCryptoCurrency(info.type,
63
- isTestnet:
64
- info.network == null ? false : info.network!.toLowerCase().contains("testnet")),
64
+ isTestnet: info.network == null
65
+ ? false
66
+ : info.network!.toLowerCase().contains("testnet")),
67
));
68
});
69
}
70
} else {
71
walletContacts.add(WalletContact(
72
info.address,
71
- _createName(info.name, "", key: [WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type) ? 0 : null),
73
+ _createName(info.name, "",
74
+ key: [WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type)
75
+ ? 0
76
+ : null),
77
walletTypeToCryptoCurrency(info.type),
78
));
79
}
@@ -82,8 +87,11 @@ abstract class ContactListViewModelBase with Store {
87
}
88
89
String _createName(String walletName, String label, {int? key = null}) {
85
- final actualLabel = label.replaceAll(RegExp(r'active', caseSensitive: false), S.current.active).replaceAll(RegExp(r'silent payments', caseSensitive: false), S.current.silent_payments);
86
- return '$walletName${key == null ? "" : " [#${key}]"} ${actualLabel.isNotEmpty ? "($actualLabel)" : ""}'.trim();
90
+ final actualLabel = label
91
+ .replaceAll(RegExp(r'active', caseSensitive: false), S.current.active)
92
+ .replaceAll(RegExp(r'silent payments', caseSensitive: false), S.current.silent_payments);
93
+ return '$walletName${key == null ? "" : " [#${key}]"} ${actualLabel.isNotEmpty ? "($actualLabel)" : ""}'
94
+ .trim();
95
}
96
97
final bool isAutoGenerateEnabled;
@@ -108,18 +116,19 @@ abstract class ContactListViewModelBase with Store {
116
Future<void> delete(ContactRecord contact) async => contact.original.delete();
117
118
ObservableList<ContactRecord> get contactsToShow =>
111
- ObservableList.of(contacts.where((element) => _isValidForCurrency(element)));
119
+ ObservableList.of(contacts.where((element) => _isValidForCurrency(element, false)));
120
121
@computed
122
List<WalletContact> get walletContactsToShow =>
115
- walletContacts.where((element) => _isValidForCurrency(element)).toList();
123
+ walletContacts.where((element) => _isValidForCurrency(element, true)).toList();
124
117
- bool _isValidForCurrency(ContactBase element) {
118
- if (element.name.contains('Silent Payments')) return false;
119
- if (element.name.contains('MWEB')) return false;
125
+ bool _isValidForCurrency(ContactBase element, bool isWalletContact) {
126
+ if (_currency == null) return true;
127
+ if (!element.name.contains('Active') &&
128
+ isWalletContact &&
129
+ (element.type == CryptoCurrency.btc || element.type == CryptoCurrency.ltc)) return false;
130
121
- return _currency == null ||
122
- element.type == _currency ||
131
+ return element.type == _currency ||
132
(element.type.tag != null &&
133
_currency?.tag != null &&
134
element.type.tag == _currency?.tag) ||