feat: use classic GraphQL pagination system for wallet history

This commit is contained in:
poka 2024-01-07 19:37:21 +01:00
parent ef9e7b30f1
commit bc415b56e8
9 changed files with 127 additions and 89 deletions

View File

@ -18,7 +18,7 @@ query ($name: String!) {
} }
'''; ''';
const String getHistoryByAddressQ = r''' const String getHistoryByAddressRelayQ = r'''
query ($address: String!, $number: Int!, $cursor: String) { query ($address: String!, $number: Int!, $cursor: String) {
transaction_connection(where: transaction_connection(where:
{_or: [ {_or: [
@ -56,6 +56,33 @@ query ($address: String!, $number: Int!, $cursor: String) {
} }
'''; ''';
const String getHistoryByAddressQ = r'''
query ($address: String!, $number: Int!, $offset: Int!) {
transaction_aggregate(where: {_or: [{issuer_pubkey: {_eq: $address}}, {receiver_pubkey: {_eq: $address}}]}) {
aggregate {
count
}
}
transaction(where: {_or: [{issuer_pubkey: {_eq: $address}}, {receiver_pubkey: {_eq: $address}}]}, order_by: {created_at: desc}, limit: $number, offset: $offset) {
amount
comment
created_at
issuer {
pubkey
identity {
name
}
}
receiver {
pubkey
identity {
name
}
}
}
}
''';
const String getCertsReceived = r''' const String getCertsReceived = r'''
query ($address: String!) { query ($address: String!) {
certification(where: {receiver: {pubkey: {_eq: $address}}}) { certification(where: {receiver: {pubkey: {_eq: $address}}}) {

View File

@ -112,7 +112,7 @@ class WalletData extends HiveObject {
final datapod = Provider.of<V2sDatapodProvider>(homeContext, listen: false); final datapod = Provider.of<V2sDatapodProvider>(homeContext, listen: false);
final avatarUuid = const Uuid().v4(); final avatarUuid = const Uuid().v4();
await datapod.getRemoteAvatar(address, saveOnDisk: true, uuid: avatarUuid); await datapod.getRemoteAvatar(address, uuid: avatarUuid);
final avatarPath = '${avatarsDirectory.path}/$address-$avatarUuid'; final avatarPath = '${avatarsDirectory.path}/$address-$avatarUuid';
if (!await File(avatarPath).exists()) return; if (!await File(avatarPath).exists()) return;

View File

@ -11,11 +11,10 @@ import 'package:graphql_flutter/graphql_flutter.dart';
class DuniterIndexer with ChangeNotifier { class DuniterIndexer with ChangeNotifier {
Map<String, String?> walletNameIndexer = {}; Map<String, String?> walletNameIndexer = {};
String? fetchMoreCursor;
Map? pageInfo;
List? transBC; List? transBC;
List listIndexerEndpoints = []; List listIndexerEndpoints = [];
bool isLoadingIndexer = false; bool isLoadingIndexer = false;
bool hasNextPage = false;
void reload() { void reload() {
notifyListeners(); notifyListeners();
@ -128,14 +127,13 @@ class DuniterIndexer with ChangeNotifier {
return indexerEndpoint; return indexerEndpoint;
} }
List parseHistory(blockchainTX, pubkey) { List parseHistory(List blockchainTX, String address) {
List transBC = []; List transBC = [];
int i = 0; int i = 0;
for (final trans in blockchainTX) { for (final transaction in blockchainTX) {
final transaction = trans['node'];
final direction = final direction =
transaction['issuer_pubkey'] != pubkey ? 'RECEIVED' : 'SENT'; transaction['issuer']['pubkey'] != address ? 'RECEIVED' : 'SENT';
transBC.add(i); transBC.add(i);
transBC[i] = []; transBC[i] = [];
@ -143,10 +141,10 @@ class DuniterIndexer with ChangeNotifier {
final amountBrut = transaction['amount']; final amountBrut = transaction['amount'];
final amount = removeDecimalZero(amountBrut / 100); final amount = removeDecimalZero(amountBrut / 100);
if (direction == "RECEIVED") { if (direction == "RECEIVED") {
transBC[i].add(transaction['issuer_pubkey']); transBC[i].add(transaction['issuer']['pubkey']);
transBC[i].add(transaction['issuer']['identity']?['name'] ?? ''); transBC[i].add(transaction['issuer']['identity']?['name'] ?? '');
} else if (direction == "SENT") { } else if (direction == "SENT") {
transBC[i].add(transaction['receiver_pubkey']); transBC[i].add(transaction['receiver']['pubkey']);
transBC[i].add(transaction['receiver']['identity']?['name'] ?? ''); transBC[i].add(transaction['receiver']['identity']?['name'] ?? '');
} }
transBC[i].add(amount); transBC[i].add(amount);
@ -157,38 +155,35 @@ class DuniterIndexer with ChangeNotifier {
return transBC; return transBC;
} }
FetchMoreOptions? mergeQueryResult(result, opts, pubkey, nRepositories) { FetchMoreOptions? mergeQueryResult(
final List<dynamic>? blockchainTX = {required List transactions,
(result.data['transaction_connection']['edges'] as List<dynamic>?); required FetchMoreOptions? opts,
required String address,
required int nRepositories,
required int offset}) {
// pageInfo = result.data!['transaction_connection']['pageInfo'];
// fetchMoreCursor = pageInfo!['endCursor'];
// final hasNextPage = pageInfo!['hasNextPage'];
// final hasPreviousPage = pageInfo!['hasPreviousPage'];
// log.d('endCursor: $fetchMoreCursor $hasNextPage $hasPreviousPage');
pageInfo = result.data['transaction_connection']['pageInfo']; // if (fetchMoreCursor != null) {
fetchMoreCursor = pageInfo!['endCursor']; opts = FetchMoreOptions(
final hasNextPage = pageInfo!['hasNextPage']; variables: {'offset': offset, 'number': nRepositories},
final hasPreviousPage = pageInfo!['hasPreviousPage']; updateQuery: (previousResultData, fetchMoreResultData) {
log.d('endCursor: $fetchMoreCursor $hasNextPage $hasPreviousPage'); final List<dynamic> repos = [
...previousResultData!['transaction'] as List<dynamic>,
...fetchMoreResultData!['transaction'] as List<dynamic>
];
if (fetchMoreCursor != null) { fetchMoreResultData['transaction'] = repos;
opts = FetchMoreOptions( return fetchMoreResultData;
variables: {'cursor': fetchMoreCursor, 'number': nRepositories}, },
updateQuery: (previousResultData, fetchMoreResultData) { );
final List<dynamic> repos = [ transBC = parseHistory(transactions, address);
...previousResultData!['transaction_connection']['edges'] // } else {
as List<dynamic>, // log.d("Activity start of $address");
...fetchMoreResultData!['transaction_connection']['edges'] // }
as List<dynamic>
];
fetchMoreResultData['transaction_connection']['edges'] = repos;
return fetchMoreResultData;
},
);
}
if (fetchMoreCursor != null) {
transBC = parseHistory(blockchainTX, pubkey);
} else {
log.d("Activity start of $pubkey");
}
return opts; return opts;
} }
@ -232,6 +227,8 @@ class DuniterIndexer with ChangeNotifier {
final options = QueryOptions(document: gql(query), variables: variables); final options = QueryOptions(document: gql(query), variables: variables);
// 5GMyvKsTNk9wDBy9jwKaX6mhSzmFFtpdK9KNnmrLoSTSuJHv
return await client.query(options); return await client.query(options);
} }

View File

@ -139,7 +139,7 @@ class V2sDatapodProvider with ChangeNotifier {
} }
Future<Image> getRemoteAvatar(String address, Future<Image> getRemoteAvatar(String address,
{double size = 20, bool saveOnDisk = false, String? uuid}) async { {double size = 20, String? uuid}) async {
final variables = <String, dynamic>{ final variables = <String, dynamic>{
'address': address, 'address': address,
}; };
@ -157,12 +157,8 @@ class V2sDatapodProvider with ChangeNotifier {
final sanitizedAvatar64 = final sanitizedAvatar64 =
avatar64.replaceAll('\n', '').replaceAll('\r', '').replaceAll(' ', ''); avatar64.replaceAll('\n', '').replaceAll('\r', '').replaceAll(' ', '');
if (saveOnDisk) { log.d('We save avatar for $address');
log.d('We save avatar for $address'); await saveAvatar(address, sanitizedAvatar64, uuid);
await saveAvatar(address, sanitizedAvatar64, uuid);
} else {
await cacheAvatar(address, sanitizedAvatar64);
}
return Image.memory( return Image.memory(
base64.decode(sanitizedAvatar64), base64.decode(sanitizedAvatar64),
@ -182,6 +178,30 @@ class V2sDatapodProvider with ChangeNotifier {
return await file.writeAsBytes(base64.decode(data)); return await file.writeAsBytes(base64.decode(data));
} }
// Future<File> cacheAvatar(String address, String data) async {
// // Get the list of all files in the directory
// final dir = Directory(avatarsCacheDirectory.path);
// var filesList = dir.listSync().whereType<File>().toList();
// // Sorting files by modified date, oldest first
// filesList
// .sort((a, b) => a.lastModifiedSync().compareTo(b.lastModifiedSync()));
// // If there are more than 20 files, remove the oldest ones
// while (filesList.length > 20) {
// filesList.first.deleteSync();
// filesList.removeAt(0);
// }
// // Write the new avatar file
// final file = File('${avatarsCacheDirectory.path}/$address');
// await file.writeAsBytes(base64.decode(data));
// log.d('cache files: ${filesList.length}');
// return file;
// }
Image getAvatarLocal(String address) { Image getAvatarLocal(String address) {
final avatarFile = File('${avatarsCacheDirectory.path}/$address'); final avatarFile = File('${avatarsCacheDirectory.path}/$address');
return Image.file( return Image.file(

View File

@ -32,13 +32,13 @@ class _ActivityScreenState extends State<ActivityScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final duniterIndexer = Provider.of<DuniterIndexer>(context, listen: true); Provider.of<DuniterIndexer>(context, listen: true);
return PopScope( return PopScope(
onPopInvoked: (_) { // onPopInvoked: (_) {
duniterIndexer.fetchMoreCursor = // duniterIndexer.fetchMoreCursor =
duniterIndexer.pageInfo = duniterIndexer.transBC = null; // duniterIndexer.pageInfo = duniterIndexer.transBC = null;
}, // },
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
elevation: 0, elevation: 0,

View File

@ -6,7 +6,6 @@ import 'package:gecko/globals.dart';
import 'package:gecko/models/queries_datapod.dart'; import 'package:gecko/models/queries_datapod.dart';
import 'package:gecko/models/scale_functions.dart'; import 'package:gecko/models/scale_functions.dart';
import 'package:gecko/providers/v2s_datapod.dart'; import 'package:gecko/providers/v2s_datapod.dart';
import 'package:gecko/widgets/commons/loading.dart';
import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -37,7 +36,7 @@ class DatapodAvatar extends StatelessWidget {
final client = ValueNotifier( final client = ValueNotifier(
GraphQLClient( GraphQLClient(
cache: GraphQLCache(store: HiveStore()), cache: GraphQLCache(),
link: httpLink, link: httpLink,
), ),
); );
@ -55,8 +54,8 @@ class DatapodAvatar extends StatelessWidget {
), ),
builder: (QueryResult result, {fetchMore, refetch}) { builder: (QueryResult result, {fetchMore, refetch}) {
if (result.isLoading) { if (result.isLoading) {
return const Center( return Center(
child: Loading(), child: ClipOval(child: datapod.defaultAvatar(size)),
); );
} }
final String? avatar64 = final String? avatar64 =

View File

@ -12,8 +12,7 @@ import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class HistoryQuery extends StatelessWidget { class HistoryQuery extends StatelessWidget {
const HistoryQuery({Key? key, required this.address}) const HistoryQuery({Key? key, required this.address}) : super(key: key);
: super(key: key);
final String address; final String address;
@override @override
@ -24,7 +23,6 @@ class HistoryQuery extends StatelessWidget {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
FetchMoreOptions? opts; FetchMoreOptions? opts;
int nPage = 1;
int nRepositories = 20; int nRepositories = 20;
if (indexerEndpoint == '') { if (indexerEndpoint == '') {
@ -39,7 +37,7 @@ class HistoryQuery extends StatelessWidget {
} }
final httpLink = HttpLink( final httpLink = HttpLink(
'$indexerEndpoint/v1beta1/relay', '$indexerEndpoint/v1/graphql',
); );
final client = ValueNotifier( final client = ValueNotifier(
@ -61,8 +59,8 @@ class HistoryQuery extends StatelessWidget {
document: gql(getHistoryByAddressQ), document: gql(getHistoryByAddressQ),
variables: <String, dynamic>{ variables: <String, dynamic>{
'address': address, 'address': address,
'number': 20, 'number': nRepositories,
'cursor': null 'offset': 0
}, },
), ),
builder: (QueryResult result, {fetchMore, refetch}) { builder: (QueryResult result, {fetchMore, refetch}) {
@ -73,6 +71,7 @@ class HistoryQuery extends StatelessWidget {
), ),
); );
} }
final List transactions = result.data?["transaction"];
if (result.hasException) { if (result.hasException) {
log.e('Error Indexer: ${result.exception}'); log.e('Error Indexer: ${result.exception}');
@ -84,8 +83,7 @@ class HistoryQuery extends StatelessWidget {
style: scaledTextStyle(fontSize: 18), style: scaledTextStyle(fontSize: 18),
) )
]); ]);
} else if (result } else if (transactions.isEmpty) {
.data?['transaction_connection']?['edges'].isEmpty) {
return Column(children: <Widget>[ return Column(children: <Widget>[
ScaledSizedBox(height: 50), ScaledSizedBox(height: 50),
Text( Text(
@ -95,22 +93,18 @@ class HistoryQuery extends StatelessWidget {
]); ]);
} }
if (result.isNotLoading) { final int totalTransactions =
if (duniterIndexer.fetchMoreCursor == null) nPage = 1; result.data!["transaction_aggregate"]["aggregate"]["count"];
duniterIndexer.hasNextPage =
!(transactions.length == totalTransactions);
if (nPage <= 3) { opts = duniterIndexer.mergeQueryResult(
nRepositories = 20; transactions: transactions,
} else if (nPage <= 6) { opts: opts,
nRepositories = 40; address: address,
} else if (nPage <= 12) { nRepositories: nRepositories,
nRepositories = 80; offset: transactions.length,
} else { );
nRepositories = 120;
}
nPage++;
opts = duniterIndexer.mergeQueryResult(
result, opts, address, nRepositories);
}
// Get transaction in progress if exist // Get transaction in progress if exist
String? transactionId; String? transactionId;
@ -141,13 +135,13 @@ class HistoryQuery extends StatelessWidget {
), ),
), ),
onNotification: (dynamic t) { onNotification: (dynamic t) {
if (duniterIndexer.pageInfo == null) { // if (duniterIndexer.pageInfo == null) {
duniterIndexer.reload(); // duniterIndexer.reload();
} // }
if (t is ScrollEndNotification && if (t is ScrollEndNotification &&
scrollController.position.pixels >= scrollController.position.pixels >=
scrollController.position.maxScrollExtent * 0.7 && scrollController.position.maxScrollExtent * 0.7 &&
duniterIndexer.pageInfo!['hasNextPage'] && duniterIndexer.hasNextPage &&
result.isNotLoading) { result.isNotLoading) {
fetchMore!(opts!); fetchMore!(opts!);
} }

View File

@ -5,6 +5,7 @@ import 'package:gecko/models/scale_functions.dart';
import 'package:gecko/providers/duniter_indexer.dart'; import 'package:gecko/providers/duniter_indexer.dart';
import 'package:gecko/providers/substrate_sdk.dart'; import 'package:gecko/providers/substrate_sdk.dart';
import 'package:gecko/screens/wallet_view.dart'; import 'package:gecko/screens/wallet_view.dart';
import 'package:gecko/widgets/commons/loading.dart';
import 'package:gecko/widgets/page_route_no_transition.dart'; import 'package:gecko/widgets/page_route_no_transition.dart';
import 'package:gecko/widgets/transaction_tile.dart'; import 'package:gecko/widgets/transaction_tile.dart';
import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:graphql_flutter/graphql_flutter.dart';
@ -97,14 +98,14 @@ class HistoryView extends StatelessWidget {
context: context), context: context),
]); ]);
}).toList()), }).toList()),
if (result.isLoading && duniterIndexer.pageInfo!['hasPreviousPage']) if (result.isLoading && duniterIndexer.hasNextPage)
const Row( const Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
CircularProgressIndicator(), Loading(size: 30, stroke: 3),
], ],
), ),
if (!duniterIndexer.pageInfo!['hasNextPage'] && if (!duniterIndexer.hasNextPage &&
sub.oldOwnerKeys[address]?[0] != null) sub.oldOwnerKeys[address]?[0] != null)
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 30), padding: const EdgeInsets.symmetric(vertical: 30),
@ -128,7 +129,7 @@ class HistoryView extends StatelessWidget {
), ),
Column(children: [ Column(children: [
Text( Text(
'identityMigrated:'.tr(), 'identityMigrated'.tr(),
style: scaledTextStyle( style: scaledTextStyle(
fontSize: 19, fontSize: 19,
color: Colors.green[700], color: Colors.green[700],
@ -150,7 +151,7 @@ class HistoryView extends StatelessWidget {
), ),
), ),
), ),
if (!duniterIndexer.pageInfo!['hasNextPage']) if (!duniterIndexer.hasNextPage)
Column( Column(
children: <Widget>[ children: <Widget>[
ScaledSizedBox(height: 15), ScaledSizedBox(height: 15),

View File

@ -36,7 +36,7 @@ class SearchIdentityQuery extends StatelessWidget {
final client = ValueNotifier( final client = ValueNotifier(
GraphQLClient( GraphQLClient(
cache: GraphQLCache( cache: GraphQLCache(
store: HiveStore()), // GraphQLCache(store: HiveStore()) store: HiveStore()),
link: httpLink, link: httpLink,
), ),
); );