From c6a370e4f5d3124485eeb7e3be79717466e40d84 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 20 Oct 2023 17:26:42 -0600 Subject: [PATCH] WIP group fusions on wallet home screen activity --- .../tx_v2/fusion_tx_group_card.dart | 176 ++++++++++++ .../tx_v2/transaction_v2_list.dart | 71 ++++- .../tx_v2/transaction_v2_list_item.dart | 258 ++++++++++-------- .../wallet_view/desktop_wallet_view.dart | 2 +- lib/utilities/format.dart | 15 +- 5 files changed, 392 insertions(+), 130 deletions(-) create mode 100644 lib/pages/wallet_view/transaction_views/tx_v2/fusion_tx_group_card.dart diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/fusion_tx_group_card.dart b/lib/pages/wallet_view/transaction_views/tx_v2/fusion_tx_group_card.dart new file mode 100644 index 000000000..0197ea15c --- /dev/null +++ b/lib/pages/wallet_view/transaction_views/tx_v2/fusion_tx_group_card.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/tx_icon.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; + +class FusionTxGroup { + final List transactions; + FusionTxGroup(this.transactions); +} + +class FusionTxGroupCard extends ConsumerWidget { + const FusionTxGroupCard({super.key, required this.group}); + + final FusionTxGroup group; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final walletId = group.transactions.first.walletId; + + final coin = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId).coin)); + + final currentHeight = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId).currentHeight)); + + return Material( + color: Theme.of(context).extension()!.popupBG, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(Constants.size.circularBorderRadius), + ), + child: Padding( + padding: const EdgeInsets.all(6), + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () async { + if (Util.isDesktop) { + // await showDialog( + // context: context, + // builder: (context) => DesktopDialog( + // maxHeight: MediaQuery.of(context).size.height - 64, + // maxWidth: 580, + // child: TransactionV2DetailsView( + // transaction: _transaction, + // coin: coin, + // walletId: walletId, + // ), + // ), + // ); + } else { + // unawaited( + // Navigator.of(context).pushNamed( + // TransactionV2DetailsView.routeName, + // arguments: ( + // tx: _transaction, + // coin: coin, + // walletId: walletId, + // ), + // ), + // ); + } + }, + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + TxIcon( + transaction: group.transactions.first, + coin: coin, + currentHeight: currentHeight, + ), + const SizedBox( + width: 14, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + // crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "Fusions", + style: STextStyles.itemSubtitle12(context), + ), + ), + ), + const SizedBox( + width: 10, + ), + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Builder( + builder: (_) { + return Text( + "${group.transactions.length} fusion transactions", + style: STextStyles.itemSubtitle12(context), + ); + }, + ), + ), + ), + ], + ), + const SizedBox( + height: 4, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + // crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + Format.extractDateFrom( + group.transactions.last.timestamp, + ), + style: STextStyles.label(context), + ), + ), + ), + // if (ref.watch(prefsChangeNotifierProvider + // .select((value) => value.externalCalls))) + // const SizedBox( + // width: 10, + // ), + // if (ref.watch(prefsChangeNotifierProvider + // .select((value) => value.externalCalls))) + // Flexible( + // child: FittedBox( + // fit: BoxFit.scaleDown, + // child: Builder( + // builder: (_) { + // return Text( + // "$prefix${Amount.fromDecimal( + // amount.decimal * price, + // fractionDigits: 2, + // ).fiatString( + // locale: locale, + // )} $baseCurrency", + // style: STextStyles.label(context), + // ); + // }, + // ), + // ), + // ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart index 5ba44acb2..8cdff69cb 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart @@ -13,8 +13,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/fusion_tx_group_card.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list_item.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/providers/db/main_db_provider.dart'; @@ -102,6 +104,59 @@ class _TransactionsV2ListState extends ConsumerState { return const NoTransActionsFound(); } else { _transactions.sort((a, b) => b.timestamp - a.timestamp); + + final List _txns = []; + + List fusions = []; + + for (int i = 0; i < _transactions.length; i++) { + final tx = _transactions[i]; + + if (tx.subType == TransactionSubType.cashFusion) { + if (fusions.isNotEmpty) { + final prevTime = DateTime.fromMillisecondsSinceEpoch( + fusions.last.timestamp * 1000); + final thisTime = + DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000); + + print( + "DIFFERERNCE: ${prevTime.difference(thisTime).inMinutes}"); + + if (prevTime.difference(thisTime).inMinutes > 30) { + _txns.add(FusionTxGroup(fusions)); + fusions = [tx]; + continue; + } + } + + fusions.add(tx); + } + + if (i + 1 < _transactions.length) { + final nextTx = _transactions[i + 1]; + if (nextTx.subType != TransactionSubType.cashFusion && + fusions.isNotEmpty) { + _txns.add(FusionTxGroup(fusions)); + fusions = []; + } + } + + if (tx.subType != TransactionSubType.cashFusion) { + _txns.add(tx); + } + } + + // sanity check + int count = 0; + for (final e in _txns) { + if (e is TransactionV2) { + count++; + } else if (e is FusionTxGroup) { + count += e.transactions.length; + } + } + assert(count == _transactions.length); + return RefreshIndicator( onRefresh: () async { final managerProvider = ref @@ -116,16 +171,16 @@ class _TransactionsV2ListState extends ConsumerState { shrinkWrap: true, itemBuilder: (context, index) { BorderRadius? radius; - if (_transactions.length == 1) { + if (_txns.length == 1) { radius = BorderRadius.circular( Constants.size.circularBorderRadius, ); - } else if (index == _transactions.length - 1) { + } else if (index == _txns.length - 1) { radius = _borderRadiusLast; } else if (index == 0) { radius = _borderRadiusFirst; } - final tx = _transactions[index]; + final tx = _txns[index]; return TxListItem( tx: tx, coin: manager.coin, @@ -141,24 +196,24 @@ class _TransactionsV2ListState extends ConsumerState { .background, ); }, - itemCount: _transactions.length, + itemCount: _txns.length, ) : ListView.builder( - itemCount: _transactions.length, + itemCount: _txns.length, itemBuilder: (context, index) { BorderRadius? radius; bool shouldWrap = false; - if (_transactions.length == 1) { + if (_txns.length == 1) { radius = BorderRadius.circular( Constants.size.circularBorderRadius, ); - } else if (index == _transactions.length - 1) { + } else if (index == _txns.length - 1) { radius = _borderRadiusLast; shouldWrap = true; } else if (index == 0) { radius = _borderRadiusFirst; } - final tx = _transactions[index]; + final tx = _txns[index]; if (shouldWrap) { return Column( children: [ diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list_item.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list_item.dart index 82fdce6d6..6780f7e49 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list_item.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list_item.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/exchange_view/trade_details_view.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/fusion_tx_group_card.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart'; import 'package:stackwallet/providers/global/trades_service_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; @@ -24,137 +25,156 @@ class TxListItem extends ConsumerWidget { required this.tx, this.radius, required this.coin, - }); + }) : assert(tx is TransactionV2 || tx is FusionTxGroup); - final TransactionV2 tx; + final Object tx; final BorderRadius? radius; final Coin coin; @override Widget build(BuildContext context, WidgetRef ref) { - final matchingTrades = ref - .read(tradesServiceProvider) - .trades - .where((e) => e.payInTxid == tx.txid || e.payOutTxid == tx.txid); + if (tx is TransactionV2) { + final _tx = tx as TransactionV2; - if (tx.type == TransactionType.outgoing && matchingTrades.isNotEmpty) { - final trade = matchingTrades.first; - return Container( - decoration: BoxDecoration( - color: Theme.of(context).extension()!.popupBG, - borderRadius: radius, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TransactionCardV2( - key: UniqueKey(), - transaction: tx, - ), - TradeCard( - key: Key(tx.txid + - tx.type.name + - tx.hashCode.toString() + - trade.uuid), // - trade: trade, - onTap: () async { - if (Util.isDesktop) { - await showDialog( - context: context, - builder: (context) => Navigator( - initialRoute: TradeDetailsView.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - FadePageRoute( - DesktopDialog( - maxHeight: null, - maxWidth: 580, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 16, - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Trade details", - style: STextStyles.desktopH3(context), - ), - DesktopDialogCloseButton( - onPressedOverride: Navigator.of( - context, - rootNavigator: true, - ).pop, - ), - ], - ), - ), - Flexible( - child: TradeDetailsView( - tradeId: trade.tradeId, - // TODO - // transactionIfSentFromStack: tx, - transactionIfSentFromStack: null, - walletName: ref.watch( - walletsChangeNotifierProvider.select( - (value) => value - .getManager(tx.walletId) - .walletName, - ), + final matchingTrades = ref + .read(tradesServiceProvider) + .trades + .where((e) => e.payInTxid == _tx.txid || e.payOutTxid == _tx.txid); + + if (_tx.type == TransactionType.outgoing && matchingTrades.isNotEmpty) { + final trade = matchingTrades.first; + return Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: radius, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TransactionCardV2( + key: UniqueKey(), + transaction: _tx, + ), + TradeCard( + key: Key(_tx.txid + + _tx.type.name + + _tx.hashCode.toString() + + trade.uuid), // + trade: trade, + onTap: () async { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + maxHeight: null, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Trade details", + style: + STextStyles.desktopH3(context), + ), + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], ), - walletId: tx.walletId, ), - ), - ], + Flexible( + child: TradeDetailsView( + tradeId: trade.tradeId, + // TODO + // transactionIfSentFromStack: tx, + transactionIfSentFromStack: null, + walletName: ref.watch( + walletsChangeNotifierProvider.select( + (value) => value + .getManager(_tx.walletId) + .walletName, + ), + ), + walletId: _tx.walletId, + ), + ), + ], + ), + ), + const RouteSettings( + name: TradeDetailsView.routeName, ), ), - const RouteSettings( - name: TradeDetailsView.routeName, - ), - ), - ]; - }, - ), - ); - } else { - unawaited( - Navigator.of(context).pushNamed( - TradeDetailsView.routeName, - arguments: Tuple4( - trade.tradeId, - tx, - tx.walletId, - ref - .read(walletsChangeNotifierProvider) - .getManager(tx.walletId) - .walletName, + ]; + }, ), - ), - ); - } - }, - ) - ], - ), - ); - } else { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).extension()!.popupBG, - borderRadius: radius, - ), - child: TransactionCardV2( - // this may mess with combined firo transactions - key: UniqueKey(), - transaction: tx, - ), - ); + ); + } else { + unawaited( + Navigator.of(context).pushNamed( + TradeDetailsView.routeName, + arguments: Tuple4( + trade.tradeId, + _tx, + _tx.walletId, + ref + .read(walletsChangeNotifierProvider) + .getManager(_tx.walletId) + .walletName, + ), + ), + ); + } + }, + ) + ], + ), + ); + } else { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: radius, + ), + child: TransactionCardV2( + // this may mess with combined firo transactions + key: UniqueKey(), + transaction: _tx, + ), + ); + } } + + final group = tx as FusionTxGroup; + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: radius, + ), + child: FusionTxGroupCard( + // this may mess with combined firo transactions + key: UniqueKey(), + group: group, + ), + ); } } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart index b9398cc66..26e066ad9 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -446,7 +446,7 @@ class _DesktopWalletViewState extends ConsumerState { .getManager(widget.walletId) .hasTokenSupport)) ? "Tokens" - : "Recent transactions", + : "Recent activity", style: STextStyles.desktopTextExtraSmall(context) .copyWith( color: Theme.of(context) diff --git a/lib/utilities/format.dart b/lib/utilities/format.dart index 2139a750e..216be7099 100644 --- a/lib/utilities/format.dart +++ b/lib/utilities/format.dart @@ -50,16 +50,27 @@ abstract class Format { // } // format date string from unix timestamp - static String extractDateFrom(int timestamp, {bool localized = true}) { + static String extractDateFrom( + int timestamp, { + bool localized = true, + bool noTime = false, + }) { var date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); if (!localized) { date = date.toUtc(); } + final dayAndYear = + "${date.day} ${Constants.monthMapShort[date.month]} ${date.year}"; + + if (noTime) { + return dayAndYear; + } + final minutes = date.minute < 10 ? "0${date.minute}" : date.minute.toString(); - return "${date.day} ${Constants.monthMapShort[date.month]} ${date.year}, ${date.hour}:$minutes"; + return "$dayAndYear, ${date.hour}:$minutes"; } // static String localizedStringAsFixed({