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>
This commit is contained in:
Serhii 2024-11-07 03:26:14 +02:00 committed by GitHub
parent 459f0d352d
commit 109d9b458e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 588 additions and 236 deletions

View file

@ -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;
}

View file

@ -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<Contact>
with Store
implements ContactBase {
abstract class ContactRecordBase extends Record<Contact> with Store implements ContactBase {
ContactRecordBase(Box<Contact> 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<Contact>
@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

View file

@ -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';

View file

@ -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;
}
}

View file

@ -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<ContactPageBody> createState() => _ContactPageBodyState();
}
class _ContactPageBodyState extends State<ContactPageBody> 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: <Widget>[
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<CakeTextTheme>()!.titleColor,
),
),
))
),
],
),
),
);
}
Future<bool> showNameAndAddressDialog(BuildContext context, String name, String address) async {
return await showPopUp<bool>(
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<ContactListBody> createState() => _ContactListBodyState();
}
class _ContactListBodyState extends State<ContactListBody> {
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<void>(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: <Widget>[
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<CakeTextTheme>()!.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<void>(
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<ExchangePageTheme>()!.buttonBackgroundColor,
),
child: filterIcon,
),
),
),
),
),
),
);
}
Future<bool> showAlertDialog(BuildContext context) async {
return await showPopUp<bool>(
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,
),
],
);
}

View file

@ -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<FilterListWidget> {
late bool ascending;
late WalletListOrderType? type;
late FilterListOrderType? type;
@override
void initState() {
@ -37,7 +37,7 @@ class FilterListWidgetState extends State<FilterListWidget> {
type = widget.initalType;
}
void setSelectedOrderType(WalletListOrderType? orderType) {
void setSelectedOrderType(FilterListOrderType? orderType) {
setState(() {
type = orderType;
});
@ -72,7 +72,7 @@ class FilterListWidgetState extends State<FilterListWidget> {
),
),
),
if (type != WalletListOrderType.Custom) ...[
if (type != FilterListOrderType.Custom) ...[
sectionDivider,
SettingsChoicesCell(
ChoicesListItem<ListOrderMode>(
@ -89,10 +89,10 @@ class FilterListWidgetState extends State<FilterListWidget> {
],
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<CakeTextTheme>()!.titleColor,
fontSize: 16,
@ -104,10 +104,10 @@ class FilterListWidgetState extends State<FilterListWidget> {
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<CakeTextTheme>()!.titleColor,
fontSize: 16,
@ -119,10 +119,10 @@ class FilterListWidgetState extends State<FilterListWidget> {
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<CakeTextTheme>()!.titleColor,
fontSize: 16,
@ -134,10 +134,10 @@ class FilterListWidgetState extends State<FilterListWidget> {
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<CakeTextTheme>()!.titleColor,
fontSize: 16,

View file

@ -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<dynamic> 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<FilteredList> {
@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,
),
);
}
}
}

View file

@ -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);
},

View file

@ -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<Widget> items, int sectionIndex) {
if (sectionTitleBuilder == null) {
throw Exception('Cannot to build title. sectionTitleBuilder is null');
}
return sectionTitleBuilder!.call(sectionIndex);
}
@override
List<Widget> buildSection(int itemCount, List<Widget> items, int sectionIndex) {
final List<Widget> section = [];
for (var itemIndex = 0; itemIndex < itemCount; itemIndex++) {
final item = itemBuilder(sectionIndex, itemIndex);
section.add(StandardListSeparator());
section.add(item);
}
return section;
}
}

View file

@ -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;

View file

@ -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<Contact> contactsSourceCopy = contacts.map((e) => e.original).toList();
await reorderContacts(contactsSourceCopy);
}
void reorderAccordingToContactList() =>
settingsStore.contactListOrder = FilterListOrderType.Custom;
Future<void> reorderContacts(List<Contact> contactCopy) async {
await contactSource.deleteAll(contactCopy.map((e) => e.key).toList());
await contactSource.addAll(contactCopy);
}
Future<void> sortGroupByType() async {
List<Contact> 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<void> sortAlphabetically() async {
List<Contact> contactsSourceCopy = contactSource.values.toList();
contactsSourceCopy
.sort((a, b) => ascending ? a.name.compareTo(b.name) : b.name.compareTo(a.name));
await reorderContacts(contactsSourceCopy);
}
Future<void> sortByCreationDate() async {
List<Contact> 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<void> 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;
}
}
}

View file

@ -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<void> 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);
}
}

View file

@ -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<WalletInfo> walletInfoSourceCopy = _walletInfoSource.values.toList();
@ -186,22 +186,22 @@ abstract class WalletListViewModelBase with Store {
_appStore.settingsStore.walletListAscending = ascending;
}
Future<void> setOrderType(WalletListOrderType? type) async {
Future<void> 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;

View file

@ -937,5 +937,6 @@
"you_pay": "انت تدفع",
"you_will_get": "حول الى",
"you_will_send": "تحويل من",
"yy": "YY"
}
"yy": "YY",
"contact_name_exists": " .ﻒﻠﺘﺨﻣ ﻢﺳﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ .ﻞﻌﻔﻟﺎﺑ ﺓﺩﻮﺟﻮﻣ ﻢﺳﻻﺍ ﺍﺬﻬﺑ ﻝﺎﺼﺗﺍ ﺔﻬﺟ"
}

View file

@ -937,5 +937,6 @@
"you_pay": "Вие плащате",
"you_will_get": "Обръщане в",
"you_will_send": "Обръщане от",
"yy": "гг"
}
"yy": "гг",
"contact_name_exists": "Вече съществува контакт с това име. Моля, изберете друго име."
}

View file

@ -937,5 +937,6 @@
"you_pay": "Zaplatíte",
"you_will_get": "Směnit na",
"you_will_send": "Směnit z",
"yy": "YY"
}
"yy": "YY",
"contact_name_exists": "Kontakt s tímto jménem již existuje. Vyberte prosím jiný název."
}

View file

@ -940,5 +940,6 @@
"you_pay": "You Pay",
"you_will_get": "Convert to",
"you_will_send": "Convert from",
"yy": "YY"
}
"yy": "YY",
"contact_name_exists": "A contact with that name already exists. Please choose a different name."
}

View file

@ -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."
}

View file

@ -937,5 +937,6 @@
"you_pay": "Vous payez",
"you_will_get": "Convertir vers",
"you_will_send": "Convertir depuis",
"yy": "AA"
}
"yy": "AA",
"contact_name_exists": "Un contact portant ce nom existe déjà. Veuillez choisir un autre nom."
}

View file

@ -939,5 +939,6 @@
"you_pay": "Ka Bayar",
"you_will_get": "Maida zuwa",
"you_will_send": "Maida daga",
"yy": "YY"
}
"yy": "YY",
"contact_name_exists": "An riga an sami lamba tare da wannan sunan. Da fatan za a zaɓi suna daban."
}

View file

@ -939,5 +939,6 @@
"you_pay": "आप भुगतान करते हैं",
"you_will_get": "में बदलें",
"you_will_send": "से रूपांतरित करें",
"yy": "वाईवाई"
}
"yy": "वाईवाई",
"contact_name_exists": "उस नाम का एक संपर्क पहले से मौजूद है. कृपया कोई भिन्न नाम चुनें."
}

View file

@ -937,5 +937,6 @@
"you_pay": "Vi plaćate",
"you_will_get": "Razmijeni u",
"you_will_send": "Razmijeni iz",
"yy": "GG"
}
"yy": "GG",
"contact_name_exists": "Kontakt s tim imenom već postoji. Odaberite drugo ime."
}

View file

@ -940,5 +940,6 @@
"you_pay": "Anda Membayar",
"you_will_get": "Konversi ke",
"you_will_send": "Konversi dari",
"yy": "YY"
}
"yy": "YY",
"contact_name_exists": "Kontak dengan nama tersebut sudah ada. Silakan pilih nama lain."
}

View file

@ -940,5 +940,6 @@
"you_pay": "Tu paghi",
"you_will_get": "Converti a",
"you_will_send": "Conveti da",
"yy": "YY"
}
"yy": "YY",
"contact_name_exists": "Esiste già un contatto con quel nome. Scegli un nome diverso."
}

View file

@ -938,5 +938,6 @@
"you_pay": "あなたが支払う",
"you_will_get": "に変換",
"you_will_send": "から変換",
"yy": "YY"
}
"yy": "YY",
"contact_name_exists": "その名前の連絡先はすでに存在します。別の名前を選択してください。"
}

View file

@ -939,5 +939,6 @@
"you_will_get": "로 변환하다",
"you_will_send": "다음에서 변환",
"YY": "YY",
"yy": "YY"
}
"yy": "YY",
"contact_name_exists": "해당 이름을 가진 연락처가 이미 존재합니다. 다른 이름을 선택하세요."
}

View file

@ -937,5 +937,6 @@
"you_pay": "သင်ပေးချေပါ။",
"you_will_get": "သို့ပြောင်းပါ။",
"you_will_send": "မှပြောင်းပါ။",
"yy": "YY"
}
"yy": "YY",
"contact_name_exists": "ထိုအမည်နှင့် အဆက်အသွယ်တစ်ခု ရှိနှင့်ပြီးဖြစ်သည်။ အခြားအမည်တစ်ခုကို ရွေးပါ။"
}

View file

@ -938,5 +938,6 @@
"you_pay": "U betaalt",
"you_will_get": "Converteren naar",
"you_will_send": "Converteren van",
"yy": "JJ"
}
"yy": "JJ",
"contact_name_exists": "Er bestaat al een contact met die naam. Kies een andere naam."
}

View file

@ -937,5 +937,6 @@
"you_pay": "Płacisz",
"you_will_get": "Konwertuj na",
"you_will_send": "Konwertuj z",
"yy": "RR"
}
"yy": "RR",
"contact_name_exists": "Kontakt o tej nazwie już istnieje. Proszę wybrać inną nazwę."
}

View file

@ -941,4 +941,4 @@
"you_will_get": "Converter para",
"you_will_send": "Converter de",
"yy": "aa"
}
}

View file

@ -938,5 +938,6 @@
"you_pay": "Вы платите",
"you_will_get": "Конвертировать в",
"you_will_send": "Конвертировать из",
"yy": "ГГ"
}
"yy": "ГГ",
"contact_name_exists": "Контакт с таким именем уже существует. Пожалуйста, выберите другое имя."
}

View file

@ -937,5 +937,6 @@
"you_pay": "คุณจ่าย",
"you_will_get": "แปลงเป็น",
"you_will_send": "แปลงจาก",
"yy": "ปี"
}
"yy": "ปี",
"contact_name_exists": "มีผู้ติดต่อชื่อนั้นอยู่แล้ว โปรดเลือกชื่ออื่น"
}

View file

@ -938,4 +938,4 @@
"you_will_get": "I-convert sa",
"you_will_send": "I-convert mula sa",
"yy": "YY"
}
}

View file

@ -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"
}
"yy": "YY",
"contact_name_exists": "Bu isimde bir kişi zaten mevcut. Lütfen farklı bir ad seçin."
}

View file

@ -938,5 +938,6 @@
"you_pay": "Ви платите",
"you_will_get": "Конвертувати в",
"you_will_send": "Конвертувати з",
"yy": "YY"
}
"yy": "YY",
"contact_name_exists": "Контакт із такою назвою вже існує. Виберіть інше ім'я."
}

View file

@ -939,5 +939,6 @@
"you_pay": "تم ادا کرو",
"you_will_get": "میں تبدیل کریں۔",
"you_will_send": "سے تبدیل کریں۔",
"yy": "YY"
}
"yy": "YY",
"contact_name_exists": " ۔ﮟﯾﺮﮐ ﺐﺨﺘﻨﻣ ﻡﺎﻧ ﻒﻠﺘﺨﻣ ﮏﯾﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔ﮯﮨ ﺩﻮﺟﻮﻣ ﮯﺳ ﮯﻠﮩﭘ ﮧﻄﺑﺍﺭ ﮏﯾﺍ ﮫﺗﺎﺳ ﮯﮐ ﻡﺎﻧ ﺱﺍ"
}

View file

@ -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"
}
"yy": "Ọd",
"contact_name_exists": "Olubasọrọ pẹlu orukọ yẹn ti wa tẹlẹ. Jọwọ yan orukọ ti o yatọ."
}

View file

@ -937,5 +937,6 @@
"you_pay": "你付钱",
"you_will_get": "转换到",
"you_will_send": "转换自",
"yy": "YY"
}
"yy": "YY",
"contact_name_exists": "已存在具有该名称的联系人。请选择不同的名称。"
}