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) ||