diff --git a/assets/translations/en.json b/assets/translations/en.json index 7cc29e7f8b92a9ff9c1ed7267270e25e5a033d77..1ff918b203e92b3ba6e677157e5f3dd3bde93705 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -94,5 +94,6 @@ "transaction_sending": "Sending", "transaction_receiving": "Receiving", "transaction_sent": "Sent", - "transaction_received": "Received" + "transaction_received": "Received", + "valid_comment": "The comment cannot have accents or commas" } diff --git a/assets/translations/es.json b/assets/translations/es.json index cb7388144c32bd2e3735b209f090d133915f7f7d..2c00c5a4f0616030b86b3e646d8f0352503c726f 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -93,5 +93,6 @@ "transaction_sending": "Enviando", "transaction_receiving": "Recibiendo", "transaction_sent": "Enviado", - "transaction_received": "Recibido" + "transaction_received": "Recibido", + "valid_comment": "El comentario no puede tener tildes o comas" } diff --git a/lib/data/models/transaction.dart b/lib/data/models/transaction.dart index 57eaccea013139d5805ee417586bdf611b3d836d..1a4bbfb2293d1f2d434869e5844b8e0c57071417 100644 --- a/lib/data/models/transaction.dart +++ b/lib/data/models/transaction.dart @@ -41,6 +41,12 @@ class Transaction extends Equatable { final Uint8List? fromAvatar; final String? fromNick; + bool get isOutgoing => + type == TransactionType.sending || type == TransactionType.sent; + + bool get isIncoming => + type == TransactionType.receiving || type == TransactionType.received; + Map<String, dynamic> toJson() => _$TransactionToJson(this); @override diff --git a/lib/g1/transaction_parser.dart b/lib/g1/transaction_parser.dart index 831b624aac733a75e42f8ac7f2bc521f827899d0..660397144c236c704af6a561d34bb54260448678 100644 --- a/lib/g1/transaction_parser.dart +++ b/lib/g1/transaction_parser.dart @@ -82,12 +82,12 @@ TransactionsAndBalanceState transactionsGvaParser(Map<String, dynamic> txData) { for (final dynamic receiveRaw in receiving) { final Transaction tx = _txGvaParse( receiveRaw as Map<String, dynamic>, TransactionType.receiving); - txs.add(tx); + txs.insert(0, tx); } for (final dynamic sendingRaw in sending) { final Transaction tx = _txGvaParse( sendingRaw as Map<String, dynamic>, TransactionType.sending); - txs.add(tx); + txs.insert(0, tx); } return TransactionsAndBalanceState( transactions: txs, balance: amount, lastChecked: DateTime.now()); diff --git a/lib/main.dart b/lib/main.dart index 9b7ec5b32b04fd4fc10629af83440d1f2a3fb8b2..dbe44ab793489160f45cebf5905abf563f252a47 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -215,6 +215,7 @@ class _GinkgoAppState extends State<GinkgoApp> { await fetchDuniterNodes(NodeType.duniter); await fetchCesiumPlusNodes(); await fetchDuniterNodes(NodeType.gva); + _printNodeStatus(prefix: 'Continuing'); } @@ -248,6 +249,10 @@ class _GinkgoAppState extends State<GinkgoApp> { Once.runCustom('clear_errors', callback: () { NodeManager().cleanErrorStats(); }, duration: const Duration(minutes: 90)); + Once.runCustom('fetch_transactions', callback: () { + fetchTransactions(context); + }, duration: const Duration(minutes: 10)); + fetchTransactions(context); return ConnectivityAppWrapper( app: MaterialApp( /// Localization is not available for the title. diff --git a/lib/ui/screens/g1_textfield.dart b/lib/ui/screens/g1_textfield.dart index 4fa30a23665a41300fd0e73f17ffa0e7a4ee90c9..c9bddc0df29528e121a318c350737bb64a5094ca 100644 --- a/lib/ui/screens/g1_textfield.dart +++ b/lib/ui/screens/g1_textfield.dart @@ -21,8 +21,6 @@ class _G1PayAmountFieldState extends State<G1PayAmountField> { @override Widget build(BuildContext context) => BlocBuilder<PaymentCubit, PaymentState>( builder: (BuildContext context, PaymentState state) { - final NumberFormat format = - NumberFormat.decimalPattern(context.locale.toString()); if (state.amount != null) { final String amountFormatted = localizeNumber(context, state.amount!); if (_controller.text != amountFormatted) { @@ -45,10 +43,13 @@ class _G1PayAmountFieldState extends State<G1PayAmountField> { onEditingComplete: () {}, onChanged: (String? value) { final bool? validate = _formKey.currentState?.validate(); - if (validate != null && validate) { - context - .read<PaymentCubit>() - .selectAmount(format.parse(value!).toDouble()); + if (validate != null && + value != null && + value.isNotEmpty && + validate) { + context.read<PaymentCubit>().selectAmount( + parseToDoubleLocalized( + context.locale.toLanguageTag(), value)); } }, decoration: InputDecoration( diff --git a/lib/ui/screens/pay_form.dart b/lib/ui/screens/pay_form.dart index 42b00a81fd0bbb75004a09cba352c760dc43cdf2..e1f334f54a511a2c24f746abf651ecd6c6dbf5f6 100644 --- a/lib/ui/screens/pay_form.dart +++ b/lib/ui/screens/pay_form.dart @@ -1,12 +1,12 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../data/models/payment_cubit.dart'; import '../../data/models/payment_state.dart'; import '../../data/models/transaction_cubit.dart'; import '../../g1/api.dart'; +import '../logger.dart'; import '../ui_helpers.dart'; import 'g1_textfield.dart'; @@ -21,112 +21,123 @@ class _PayFormState extends State<PayForm> { final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); final TextEditingController _commentController = TextEditingController(); +// static final RegExp _englishRegExp = RegExp('^[\u0000-\u007F]*\$'); + // static final RegExp _englishRegExp = RegExp(r'^[a-zA-Z0-9\s.,;:!?()\-]*$'); + @override Widget build(BuildContext context) { return BlocBuilder<PaymentCubit, PaymentState>( builder: (BuildContext context, PaymentState state) { - if (state.comment != null && - _commentController.text != state.comment) { - _commentController.text = state.comment; - } - return Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: <Widget>[ - const G1PayAmountField(), - const SizedBox(height: 10.0), - TextField( - controller: _commentController, - inputFormatters: <TextInputFormatter>[ - Iso88591TextInputFormatter() - ], - onChanged: (String? value) { - if (value != null) { - context.read<PaymentCubit>().setComment(value); - } - }, - decoration: InputDecoration( - labelText: tr('g1_form_pay_desc'), - hintText: tr('g1_form_pay_hint'), - border: const OutlineInputBorder(), - ), - maxLines: null, - ), - const SizedBox(height: 10.0), - ElevatedButton( - onPressed: !state.canBeSent() || + if (state.comment != null && _commentController.text != state.comment) { + _commentController.text = state.comment; + } + return Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: <Widget>[ + const G1PayAmountField(), + const SizedBox(height: 10.0), + TextFormField( + controller: _commentController, + onChanged: (String? value) { + final bool? validate = _commentValidate(); + if (validate != null && + value != null && + value.isNotEmpty && + validate) { + context.read<PaymentCubit>().setComment(value); + } + }, + decoration: InputDecoration( + labelText: tr('g1_form_pay_desc'), + hintText: tr('g1_form_pay_hint'), + border: const OutlineInputBorder(), + ), + validator: (String? value) { + if (value != null && !basicEnglishCharsRegExp.hasMatch(value)) { + return tr('valid_comment'); + } + return null; + }, + maxLines: null, + ), + const SizedBox(height: 10.0), + ElevatedButton( + onPressed: !state.canBeSent() || + _commentValidate() == false || state.amount == null || !_weHaveBalance(context, state.amount!) - ? null - : () async { - // We disable the number, anyway - context.read<PaymentCubit>().sending(); - final bool? confirmed = await _confirmSend( - context, - state.amount!.toString(), - humanizePubKey(state.publicKey)); - if (!mounted) { - return; - } - if (confirmed == null || !confirmed) { - context.read<PaymentCubit>().sentFailed(); - } else { - final String response = await pay( - to: state.publicKey, - comment: state.comment, - amount: state.amount!); + ? null + : () async { + // We disable the number, anyway + context.read<PaymentCubit>().sending(); + final bool? confirmed = await _confirmSend( + context, + state.amount!.toString(), + humanizePubKey(state.publicKey)); if (!mounted) { - // Cannot show a tooltip if the widget is not now visible return; } - if (response == 'success') { - context.read<PaymentCubit>().sent(); - showTooltip(context, '', tr('payment_successful')); - } else { + if (confirmed == null || !confirmed) { context.read<PaymentCubit>().sentFailed(); - showTooltip(context, '', tr(response)); + } else { + final String response = await pay( + to: state.publicKey, + comment: state.comment, + amount: state.amount!); + if (!mounted) { + // Cannot show a tooltip if the widget is not now visible + return; + } + if (response == 'success') { + context.read<PaymentCubit>().sent(); + showTooltip(context, '', tr('payment_successful')); + } else { + context.read<PaymentCubit>().sentFailed(); + showTooltip(context, '', tr(response)); + } } - } - }, - style: ElevatedButton.styleFrom( - padding: + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 25), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30.0), - ), - foregroundColor: Colors.white, - backgroundColor: Theme - .of(context) - .colorScheme - .primary, - textStyle: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: <Widget>[ - const Icon(Icons.send), - const SizedBox(width: 10), - Text(tr('g1_form_pay_send')), - ], - ), - ) - ], - ), - ); - }); + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.0), + ), + foregroundColor: Colors.white, + backgroundColor: Theme.of(context).colorScheme.primary, + textStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: <Widget>[ + const Icon(Icons.send), + const SizedBox(width: 10), + Text(tr('g1_form_pay_send')), + ], + ), + ) + ], + ), + ); + }); + } + + bool _commentValidate() { + final bool? val = _formKey.currentState?.validate(); + logger('Validating comment: $val'); + return val ?? false; } bool _weHaveBalance(BuildContext context, double amount) => - context - .read<TransactionsCubit>() - .balance >= amount * 100; + context.read<TransactionsCubit>().balance >= amount * 100; - Future<bool?> _confirmSend(BuildContext context, String amount, - String to) async { + Future<bool?> _confirmSend( + BuildContext context, String amount, String to) async { return showDialog<bool>( context: context, builder: (BuildContext context) { @@ -149,18 +160,3 @@ class _PayFormState extends State<PayForm> { ); } } - -class Iso88591TextInputFormatter extends TextInputFormatter { - // static final RegExp _iso88591RegExp = RegExp('^[\u0000-\u00FF]*\$'); - static final RegExp _englishRegExp = RegExp('^[\u0000-\u007F]*\$'); - - @override - TextEditingValue formatEditUpdate(TextEditingValue oldValue, - TextEditingValue newValue) { - if (_englishRegExp.hasMatch(newValue.text)) { - return newValue; - } else { - return oldValue; - } - } -} diff --git a/lib/ui/ui_helpers.dart b/lib/ui/ui_helpers.dart index 0115eda8aae525103e6e540f831f7d4607c4cad1..53fd3714dfcf4aa219844aa3749c23578a50f4a1 100644 --- a/lib/ui/ui_helpers.dart +++ b/lib/ui/ui_helpers.dart @@ -3,10 +3,12 @@ import 'dart:typed_data'; import 'package:clipboard/clipboard.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:timeago/timeago.dart' as timeago; import '../data/models/contact.dart'; -import '../data/models/transaction_type.dart'; +import '../data/models/node_list_cubit.dart'; +import '../data/models/transaction_cubit.dart'; import '../g1/api.dart'; import '../shared_prefs.dart'; import 'widgets/first_screen/circular_icon.dart'; @@ -41,10 +43,12 @@ const Color defAvatarBgColor = Colors.grey; const Color defAvatarColor = Colors.white; Widget avatar(Uint8List? rawAvatar, - {Color color = defAvatarColor, Color bgColor = defAvatarBgColor}) { + {Color color = defAvatarColor, + Color bgColor = defAvatarBgColor, + double avatarSize = 24}) { return rawAvatar != null && rawAvatar.isNotEmpty ? CircleAvatar( - radius: 24, + radius: avatarSize, child: ClipOval( child: Image.memory( rawAvatar, @@ -62,6 +66,24 @@ String humanizeFromToPubKey(String publicAddress, String address) { } } +String humanizeContact(String publicAddress, Contact contact) { + final bool hasName = contact.name?.isNotEmpty ?? false; + final bool hasNick = contact.nick?.isNotEmpty ?? false; + + if (contact.pubKey == publicAddress) { + return tr('your_wallet'); + } else { + if (hasName && hasNick) + return '${contact.name} (${contact.nick})'; + else if (hasNick) + return contact.nick!; + else if (hasName) + return contact.name!; + else + return humanizePubKey(contact.pubKey); + } +} + String humanizePubKey(String address) => '\u{1F511} ${simplifyPubKey(address)}'; String simplifyPubKey(String address) => address.substring(0, 8); @@ -82,8 +104,9 @@ Color tileColor(int index, BuildContext context, [bool inverse = false]) { : unselectedColor; } +// https://github.com/andresaraujo/timeago.dart/pull/142#issuecomment-859661123 String? humanizeTime(DateTime time, String locale) => - timeago.format(time, locale: locale, clock: DateTime.now()); + timeago.format(time.toUtc(), locale: locale, clock: DateTime.now().toUtc()); const bool txDebugging = false; @@ -107,19 +130,14 @@ String formatAmount(BuildContext context, double amount) { String formatKAmount(BuildContext context, double amount) => formatAmount(context, amount / 100); +double parseToDoubleLocalized(String locale, String double) => + NumberFormat.decimalPattern(locale).parse(double).toDouble(); + String getAppVersion() => '0.0.8'; String localizeNumber(BuildContext context, double amount) => NumberFormat.decimalPattern(context.locale.toString()).format(amount); -bool isOutgoing(TransactionType type) { - return type == TransactionType.sending || type == TransactionType.sent; -} - -bool isIncoming(TransactionType type) { - return type == TransactionType.receiving || type == TransactionType.received; -} - Contact contactFromResultSearch(Map<String, dynamic> record) { final Map<String, dynamic> source = record['_source'] as Map<String, dynamic>; final Uint8List? avatarBase64 = _getAvatarFromResults(source); @@ -147,3 +165,13 @@ Uint8List? _getAvatarFromResults(Map<String, dynamic> source) { } return avatarBase64; } + +final RegExp basicEnglishCharsRegExp = + RegExp(r'^[ A-Za-z0-9\s.;:!?()\-_;!@&<>%]*$'); +// RegExp(r'^[a-zA-Z0-9-_:/;*\[\]()?!^\\+=@&~#{}|\<>%.]*$'); + +void fetchTransactions(BuildContext context) { + final TransactionsCubit transCubit = context.read<TransactionsCubit>(); + final NodeListCubit nodeListCubit = context.read<NodeListCubit>(); + transCubit.fetchTransactions(nodeListCubit); +} diff --git a/lib/ui/widgets/fifth_screen/import_dialog.dart b/lib/ui/widgets/fifth_screen/import_dialog.dart index 9d4bdc89a0110dae5bda6cc7c959204f4cb16ece..060623729605d3836958e6be4d1cc85b2a0e4b0c 100644 --- a/lib/ui/widgets/fifth_screen/import_dialog.dart +++ b/lib/ui/widgets/fifth_screen/import_dialog.dart @@ -12,6 +12,7 @@ import '../../../data/models/transaction_cubit.dart'; import '../../../g1/g1_helper.dart'; import '../../../shared_prefs.dart'; import '../../logger.dart'; +import '../../ui_helpers.dart'; import '../custom_error_widget.dart'; import '../loading_box.dart'; import 'pattern_util.dart'; @@ -78,6 +79,8 @@ class _ImportDialogState extends State<ImportDialog> { style: const TextStyle(color: Colors.white), ), ); + // ok, fetch the transactions & balance + fetchTransactions(context); } if (!mounted) { return; diff --git a/lib/ui/widgets/fourth_screen/transaction_item.dart b/lib/ui/widgets/fourth_screen/transaction_item.dart index 3d7aefc244eb02ec5c25b2bb7d2462c261138eea..298a497c19acb72b61b2aea5d156ebce6461cd3f 100644 --- a/lib/ui/widgets/fourth_screen/transaction_item.dart +++ b/lib/ui/widgets/fourth_screen/transaction_item.dart @@ -26,33 +26,31 @@ class TransactionListItem extends StatelessWidget { @override Widget build(BuildContext context) => - BlocBuilder<TransactionsCubit, - TransactionsAndBalanceState>( + BlocBuilder<TransactionsCubit, TransactionsAndBalanceState>( builder: (BuildContext context, - TransactionsAndBalanceState transBalanceState) => - FutureBuilder<Contact>( + TransactionsAndBalanceState transBalanceState) => + FutureBuilder<List<Contact>>( future: _fetchContact(pubKey, transaction), builder: (BuildContext context, - AsyncSnapshot<Contact> snapshot) { + AsyncSnapshot<List<Contact>> snapshot) { if (snapshot.hasData) { return _buildTransactionItem(context, snapshot.data!); } else if (snapshot.hasError) { return Text('Error ${snapshot.error}'); } else { - return _buildTransactionItem(context, Contact( - pubKey: isIncoming(transaction.type) ? transaction - .from : transaction.to)); + return _buildTransactionItem(context, <Contact>[ + Contact(pubKey: transaction.from), + Contact(pubKey: transaction.to) + ]); } })); - Slidable _buildTransactionItem(BuildContext context, - Contact contact) { + Slidable _buildTransactionItem(BuildContext context, List<Contact> contacts) { IconData? icon; Color? iconColor; String statusText; final String amountS = - '${transaction.amount < 0 ? "" : "+"}${formatKAmount( - context, transaction.amount)}'; + '${transaction.amount < 0 ? "" : "+"}${formatKAmount(context, transaction.amount)}'; statusText = tr('transaction_${transaction.type.name}'); switch (transaction.type) { case TransactionType.pending: @@ -73,10 +71,10 @@ class TransactionListItem extends StatelessWidget { break; } final String myPubKey = SharedPreferencesHelper().getPubKey(); - final ContactsCubit contactsCubit = - context.read<ContactsCubit>(); + + final ContactsCubit contactsCubit = context.read<ContactsCubit>(); return Slidable( - // Specify a key if the Slidable is dismissible. + // Specify a key if the Slidable is dismissible. key: const ValueKey<int>(0), // The end action pane is the one at the right or the bottom side. endActionPane: ActionPane( @@ -84,16 +82,15 @@ class TransactionListItem extends StatelessWidget { children: <SlidableAction>[ SlidableAction( onPressed: (BuildContext c) { - contactsCubit.addContact(contact); + contactsCubit.addContact( + transaction.isIncoming ? contacts[0] : contacts[1]); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(tr('contact_added')), ), ); }, - backgroundColor: Theme - .of(context) - .primaryColor, + backgroundColor: Theme.of(context).primaryColor, foregroundColor: Colors.white, icon: Icons.contacts, label: tr('add_contact'), @@ -103,14 +100,13 @@ class TransactionListItem extends StatelessWidget { child: ListTile( leading: (icon != null) ? Icon( - icon, - color: iconColor, - ) + icon, + color: iconColor, + ) : null, tileColor: tileColor(index, context), title: Row( children: <Widget>[ - // if (avatar != null) avatar, const SizedBox(width: 8.0), Expanded( child: Column( @@ -145,11 +141,9 @@ class TransactionListItem extends StatelessWidget { child: Text( tr('transaction_from_to', namedArgs: <String, String>{ - 'from': humanizeFromToPubKey( - myPubKey, - transaction.from), - 'to': humanizeFromToPubKey( - myPubKey, transaction.to) + 'from': + humanizeContact(myPubKey, contacts[0]), + 'to': humanizeContact(myPubKey, contacts[1]) }), style: const TextStyle( fontSize: 14.0, @@ -181,18 +175,15 @@ class TransactionListItem extends StatelessWidget { amountS, style: TextStyle( // fontWeight: FontWeight.bold, - color: transaction.type == - TransactionType.received || - transaction.type == - TransactionType.receiving + color: transaction.type == TransactionType.received || + transaction.type == TransactionType.receiving ? Colors.blue : Colors.red, ), ), const SizedBox(height: 4.0), Text( - humanizeTime( - transaction.time, context.locale.toString())!, + humanizeTime(transaction.time, context.locale.toString())!, style: const TextStyle( fontSize: 12.0, color: Colors.grey, @@ -203,12 +194,15 @@ class TransactionListItem extends StatelessWidget { )); } - Future<Contact> _fetchContact(String pubKey, Transaction transaction) async { - // return Contact(pubKey: pubKey); + Future<List<Contact>> _fetchContact( + String pubKey, Transaction transaction) async { + final Contact myContact = await ContactsCache().getContact(pubKey); if (pubKey == transaction.from) { - return ContactsCache().getContact(transaction.to); + final Contact to = await ContactsCache().getContact(transaction.to); + return <Contact>[myContact, to]; } else { - return ContactsCache().getContact(transaction.from); + final Contact from = await ContactsCache().getContact(transaction.from); + return <Contact>[from, myContact]; } } } diff --git a/lib/ui/widgets/fourth_screen/transaction_page.dart b/lib/ui/widgets/fourth_screen/transaction_page.dart index 6691dea6aeb80902789b446e3abd4535c6f34633..1086ec4c0f6e222b4e0bd09f228a2752d812c77c 100644 --- a/lib/ui/widgets/fourth_screen/transaction_page.dart +++ b/lib/ui/widgets/fourth_screen/transaction_page.dart @@ -33,6 +33,7 @@ class _TransactionsAndBalanceWidgetState void initState() { super.initState(); _transScrollController.addListener(_scrollListener); +// Remove in the future transCubit = context.read<TransactionsCubit>(); nodeListCubit = context.read<NodeListCubit>(); transCubit.fetchTransactions(nodeListCubit); @@ -56,7 +57,7 @@ class _TransactionsAndBalanceWidgetState } final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey = - GlobalKey<RefreshIndicatorState>(); + GlobalKey<RefreshIndicatorState>(); @override Widget build(BuildContext context) { @@ -67,7 +68,10 @@ class _TransactionsAndBalanceWidgetState final double balance = transBalanceState.balance; return BackdropScaffold( appBar: BackdropAppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, + backgroundColor: Theme + .of(context) + .colorScheme + .inversePrimary, title: Text(tr('balance')), actions: <Widget>[ IconButton( @@ -78,49 +82,59 @@ class _TransactionsAndBalanceWidgetState }, ), const BackdropToggleButton( - // The default - // icon: AnimatedIcons.close_menu, - ) + // The default + // icon: AnimatedIcons.close_menu, + ) ], ), backLayer: Center( child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.inversePrimary, - border: Border.all( - color: Theme.of(context).colorScheme.inversePrimary, - width: 3), - /* borderRadius: const BorderRadius.only( + decoration: BoxDecoration( + color: Theme + .of(context) + .colorScheme + .inversePrimary, + border: Border.all( + color: Theme + .of(context) + .colorScheme + .inversePrimary, + width: 3), + /* borderRadius: const BorderRadius.only( topLeft: Radius.circular(8), topRight: Radius.circular(8), ), */ - ), - child: Scrollbar( - child: ListView( - // controller: scrollController, - children: <Widget>[ - Padding( - padding: const EdgeInsets.symmetric(vertical: 10.0), - child: Center( - child: Text( - formatKAmount(context, balance), - style: TextStyle( - fontSize: 36.0, - color: - balance == 0 ? Colors.lightBlue : Colors.lightBlue, - fontWeight: FontWeight.bold), - )), ), - if (!kReleaseMode) TransactionChart() - /*BalanceChart( + child: Scrollbar( + child: ListView( + // controller: scrollController, + children: <Widget>[ + Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: Center( + child: Text( + formatKAmount(context, balance), + style: TextStyle( + fontSize: 36.0, + color: + balance == 0 ? Colors.lightBlue : Colors + .lightBlue, + fontWeight: FontWeight.bold), + )), + ), + if (!kReleaseMode) TransactionChart() + /*BalanceChart( transactions: .transactions),*/ - ], - )), - )), + ], + )), + )), subHeader: BackdropSubHeader( title: Text(tr('transactions')), divider: Divider( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme + .of(context) + .colorScheme + .surfaceVariant, height: 0, ), ), @@ -140,16 +154,19 @@ class _TransactionsAndBalanceWidgetState child: Header(text: 'transactions'))), */ Expanded( child: Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 50), - child: transactions.isEmpty - ? Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 50), + child: transactions.isEmpty + ? Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Center(child: Text(tr('no_transactions')))) - : RefreshIndicator( + : RefreshIndicator( key: _refreshIndicatorKey, color: Colors.white, backgroundColor: - Theme.of(context).colorScheme.primary, + Theme + .of(context) + .colorScheme + .primary, strokeWidth: 4.0, onRefresh: () async { return _refreshTransactions(); @@ -238,7 +255,7 @@ class _TransactionsAndBalanceWidgetState */ }, )), - )) + )) ]), )); }); diff --git a/lib/ui/widgets/transaction_contact_widget.dart b/lib/ui/widgets/transaction_contact_widget.dart new file mode 100644 index 0000000000000000000000000000000000000000..c2a42b4bd23a5155e2de42fe9a14e0c5b9825115 --- /dev/null +++ b/lib/ui/widgets/transaction_contact_widget.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +import '../../data/models/contact.dart'; +import '../ui_helpers.dart'; + +class TransactionContactWidget extends StatelessWidget { + const TransactionContactWidget( + {super.key, required this.contact, this.avatarSize = 10.0}); + + final Contact contact; + final double avatarSize; + + @override + Widget build(BuildContext context) { + final bool hasName = contact.name?.isNotEmpty ?? false; + final bool hasNick = contact.nick?.isNotEmpty ?? false; + + return Row( + children: <Widget>[ + avatar(contact.avatar, avatarSize: avatarSize), + const SizedBox(width: 8.0), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: <Widget>[ + if (hasName && hasNick) + Text('${contact.name} (${contact.nick})', + style: Theme.of(context).textTheme.titleMedium) + else if (hasNick) + Text(contact.nick!, + style: Theme.of(context).textTheme.titleMedium) + else if (hasName) + Text(contact.name!, + style: Theme.of(context).textTheme.titleMedium) + else + Text(contact.pubKey, + style: Theme.of(context).textTheme.titleMedium) + ], + ), + ), + ], + ); + } +} diff --git a/test/ui_test.dart b/test/ui_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..c4543f13c5a20943ea11b1cedf4ebe48959841c2 --- /dev/null +++ b/test/ui_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:ginkgo/ui/ui_helpers.dart'; + +void main() { + test('localizedParseToDouble parses a localized double string correctly', () { + const String doubleString = '1.234,56'; + final double parsedDouble = parseToDoubleLocalized('es', doubleString); + expect(parsedDouble, equals(1234.56)); + }); + + test('valid and invalid comments', () { + const List<String> invalidText = <String>['á', '`e', 'ç', 'ñ', ',']; + const List<String> validText = <String>[ + 'ab c de', + 'a b c d e', + 'a-b', + 'a_b', + 'a%', + 'a & b' + ]; + for (final String text in invalidText) { + expect(basicEnglishCharsRegExp.hasMatch(text), equals(false), + reason: 'Failed $text'); + } + for (final String text in validText) { + expect(basicEnglishCharsRegExp.hasMatch(text), equals(true), + reason: 'Failed $text'); + } + }); +}