diff --git a/lib/data/models/contact.dart b/lib/data/models/contact.dart index 8c684d81b116156c60b30ca28e890b797b0f5319..a7b09ea540761a0f41b7b5e345034f51bbd2843d 100644 --- a/lib/data/models/contact.dart +++ b/lib/data/models/contact.dart @@ -5,6 +5,7 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'is_json_serializable.dart'; +import 'model_utils.dart'; part 'contact.g.dart'; @@ -13,7 +14,7 @@ part 'contact.g.dart'; class Contact extends Equatable implements IsJsonSerializable<Contact> { const Contact({ this.nick, - required this.pubkey, + required this.pubKey, this.avatar, this.notes, this.name, @@ -23,14 +24,14 @@ class Contact extends Equatable implements IsJsonSerializable<Contact> { _$ContactFromJson(json); final String? nick; - final String pubkey; - @JsonKey(fromJson: _fromList, toJson: _toList) + final String pubKey; + @JsonKey(fromJson: uIntFromList, toJson: uIntToList) final Uint8List? avatar; final String? notes; final String? name; @override - List<Object?> get props => <dynamic>[nick, pubkey, avatar, notes, name]; + List<Object?> get props => <dynamic>[nick, pubKey, avatar, notes, name]; @override Map<String, dynamic> toJson() => _$ContactToJson(this); @@ -38,13 +39,8 @@ class Contact extends Equatable implements IsJsonSerializable<Contact> { @override Contact fromJson(Map<String, dynamic> json) => Contact.fromJson(json); - static Uint8List _fromList(List<int> list) => Uint8List.fromList(list); - - static List<int> _toList(Uint8List? uint8List) => - uint8List != null ? uint8List.toList() : <int>[]; - @override String toString() { - return 'Contact $pubkey, hasAvatar: ${avatar != null}, nick: $nick, name: $name'; + return 'Contact $pubKey, hasAvatar: ${avatar != null}, nick: $nick, name: $name'; } } diff --git a/lib/data/models/contact.g.dart b/lib/data/models/contact.g.dart index e5bc557c0dc0d4bbb161479d92af042551d81db4..b7a39ffdadc0bd4522da342184325855e4254666 100644 --- a/lib/data/models/contact.g.dart +++ b/lib/data/models/contact.g.dart @@ -9,7 +9,7 @@ part of 'contact.dart'; abstract class _$ContactCWProxy { Contact nick(String? nick); - Contact pubkey(String pubkey); + Contact pubKey(String pubKey); Contact avatar(Uint8List? avatar); @@ -25,7 +25,7 @@ abstract class _$ContactCWProxy { /// ```` Contact call({ String? nick, - String? pubkey, + String? pubKey, Uint8List? avatar, String? notes, String? name, @@ -42,7 +42,7 @@ class _$ContactCWProxyImpl implements _$ContactCWProxy { Contact nick(String? nick) => this(nick: nick); @override - Contact pubkey(String pubkey) => this(pubkey: pubkey); + Contact pubKey(String pubKey) => this(pubKey: pubKey); @override Contact avatar(Uint8List? avatar) => this(avatar: avatar); @@ -63,7 +63,7 @@ class _$ContactCWProxyImpl implements _$ContactCWProxy { /// ```` Contact call({ Object? nick = const $CopyWithPlaceholder(), - Object? pubkey = const $CopyWithPlaceholder(), + Object? pubKey = const $CopyWithPlaceholder(), Object? avatar = const $CopyWithPlaceholder(), Object? notes = const $CopyWithPlaceholder(), Object? name = const $CopyWithPlaceholder(), @@ -73,10 +73,10 @@ class _$ContactCWProxyImpl implements _$ContactCWProxy { ? _value.nick // ignore: cast_nullable_to_non_nullable : nick as String?, - pubkey: pubkey == const $CopyWithPlaceholder() || pubkey == null - ? _value.pubkey + pubKey: pubKey == const $CopyWithPlaceholder() || pubKey == null + ? _value.pubKey // ignore: cast_nullable_to_non_nullable - : pubkey as String, + : pubKey as String, avatar: avatar == const $CopyWithPlaceholder() ? _value.avatar // ignore: cast_nullable_to_non_nullable @@ -105,16 +105,16 @@ extension $ContactCopyWith on Contact { Contact _$ContactFromJson(Map<String, dynamic> json) => Contact( nick: json['nick'] as String?, - pubkey: json['pubkey'] as String, - avatar: Contact._fromList(json['avatar'] as List<int>), + pubKey: json['pubKey'] as String, + avatar: uIntFromList(json['avatar']), notes: json['notes'] as String?, name: json['name'] as String?, ); Map<String, dynamic> _$ContactToJson(Contact instance) => <String, dynamic>{ 'nick': instance.nick, - 'pubkey': instance.pubkey, - 'avatar': Contact._toList(instance.avatar), + 'pubKey': instance.pubKey, + 'avatar': uIntToList(instance.avatar), 'notes': instance.notes, 'name': instance.name, }; diff --git a/lib/data/models/contact_cubit.dart b/lib/data/models/contact_cubit.dart index f3e76655e96eead8831fe5625a769d13ce7772b2..1093045abd55228fbf0fcff52be00114d6a79549 100644 --- a/lib/data/models/contact_cubit.dart +++ b/lib/data/models/contact_cubit.dart @@ -9,7 +9,7 @@ class ContactsCubit extends HydratedCubit<ContactsState> { Contact? _find(Contact contact) { try { return state.contacts - .firstWhere((Contact c) => c.pubkey == contact.pubkey); + .firstWhere((Contact c) => c.pubKey == contact.pubKey); } catch (e) { return null; } @@ -24,10 +24,10 @@ class ContactsCubit extends HydratedCubit<ContactsState> { void removeContact(Contact contact) { final List<Contact> contactsTruncated = state.contacts - .where((Contact c) => c.pubkey != contact.pubkey) + .where((Contact c) => c.pubKey != contact.pubKey) .toList(); final List<Contact> filteredContactsTruncated = state.filteredContacts - .where((Contact c) => c.pubkey != contact.pubkey) + .where((Contact c) => c.pubKey != contact.pubKey) .toList(); emit(state.copyWith( contacts: contactsTruncated, @@ -36,7 +36,7 @@ class ContactsCubit extends HydratedCubit<ContactsState> { void updateContact(Contact contact) { final List<Contact> contacts = state.contacts.map((Contact c) { - if (c.pubkey == contact.pubkey) { + if (c.pubKey == contact.pubKey) { return contact; } return c; @@ -50,7 +50,7 @@ class ContactsCubit extends HydratedCubit<ContactsState> { void filterContacts(String query) { final List<Contact> contacts = state.contacts.where((Contact c) { - if (c.pubkey.contains(query)) { + if (c.pubKey.contains(query)) { return true; } if (c.nick != null && c.nick!.contains(query)) { @@ -91,5 +91,5 @@ class ContactsCubit extends HydratedCubit<ContactsState> { String get id => 'contacts'; bool isContact(String pubKey) => - state.contacts.any((Contact c) => c.pubkey == pubKey); + state.contacts.any((Contact c) => c.pubKey == pubKey); } diff --git a/lib/data/models/model_utils.dart b/lib/data/models/model_utils.dart index 57d0d80e400ef067cc5aef926d1c688a116c6606..0a1525906ec7ea24011ee037095e040b2916010a 100644 --- a/lib/data/models/model_utils.dart +++ b/lib/data/models/model_utils.dart @@ -1,6 +1,15 @@ +import 'dart:convert'; import 'dart:typed_data'; -Uint8List uIntFromList(List<int> list) => Uint8List.fromList(list); +Uint8List? uIntFromList(dynamic value) { + if (value is List<int> && value.isNotEmpty) { + return Uint8List.fromList(value); + } else if (value is String && value.isNotEmpty) { + return base64Decode(value); + } else { + return null; + } +} List<int> uIntToList(Uint8List? uInt8List) => uInt8List != null ? uInt8List.toList() : <int>[]; diff --git a/lib/data/models/payment_state.g.dart b/lib/data/models/payment_state.g.dart index 11b4bd1f93acead913934524c1a9f0c56273034b..5bb586a666591a9cdace210b44e9d06ea78c68ea 100644 --- a/lib/data/models/payment_state.g.dart +++ b/lib/data/models/payment_state.g.dart @@ -9,7 +9,7 @@ part of 'payment_state.dart'; PaymentState _$PaymentStateFromJson(Map<String, dynamic> json) => PaymentState( publicKey: json['publicKey'] as String, nick: json['nick'] as String?, - avatar: uIntFromList(json['avatar'] as List<int>), + avatar: uIntFromList(json['avatar']), comment: json['comment'] as String? ?? '', amount: (json['amount'] as num?)?.toDouble(), status: $enumDecodeNullable(_$PaymentStatusEnumMap, json['status']) ?? diff --git a/lib/data/models/transaction.g.dart b/lib/data/models/transaction.g.dart index 6e57d124f37613e2ed8fb70a32e4734dfbba84b5..9442400adfcbe57bd654f92e99fb55f6ed6dad22 100644 --- a/lib/data/models/transaction.g.dart +++ b/lib/data/models/transaction.g.dart @@ -242,9 +242,9 @@ Transaction _$TransactionFromJson(Map<String, dynamic> json) => Transaction( amount: (json['amount'] as num).toDouble(), comment: json['comment'] as String, time: DateTime.parse(json['time'] as String), - toAvatar: uIntFromList(json['toAvatar'] as List<int>), + toAvatar: uIntFromList(json['toAvatar']), toNick: json['toNick'] as String?, - fromAvatar: uIntFromList(json['fromAvatar'] as List<int>), + fromAvatar: uIntFromList(json['fromAvatar']), fromNick: json['fromNick'] as String?, ); diff --git a/lib/g1/api.dart b/lib/g1/api.dart index 14ecd366cfd344fad4b8efa4b198e5427342df47..a991d5dbc298dba3774da304b0ea3cac9893ccd6 100644 --- a/lib/g1/api.dart +++ b/lib/g1/api.dart @@ -56,7 +56,7 @@ Future<Contact> getProfile(String pubKey) async { final Map<String, dynamic> result = const JsonDecoder().convert(cPlusResponse.body) as Map<String, dynamic>; if (result['found'] == false) { - return Contact(pubkey: pubKey); + return Contact(pubKey: pubKey); } final String? nick = await gvaNick(pubKey); @@ -68,7 +68,7 @@ Future<Contact> getProfile(String pubKey) async { return c.copyWith(nick: nick); } catch (e) { logger('Error in getProfile $e'); - return Contact(pubkey: pubKey); + return Contact(pubKey: pubKey); } } @@ -100,7 +100,7 @@ Future<List<Contact>> searchWot(String searchTerm) async { final String pubKey = resultMap['pubkey'] as String; // ignore: avoid_dynamic_calls final String nick = resultMap['uids'][0]['uid']! as String; - contacts.add(Contact(nick: nick, pubkey: pubKey)); + contacts.add(Contact(nick: nick, pubKey: pubKey)); } } } @@ -110,7 +110,7 @@ Future<List<Contact>> searchWot(String searchTerm) async { Future<Contact> getWot(Contact contact) async { final Response response = await requestDuniterWithRetry( - '/wot/lookup/${contact.pubkey}', + '/wot/lookup/${contact.pubKey}', retryWith404: false); // Will be better to analyze the 404 response (to detect faulty node) if (response.statusCode == HttpStatus.ok) { diff --git a/lib/ui/contacts_cache.dart b/lib/ui/contacts_cache.dart index 8568ff6349d5b25c7068ed16b02b6d42ba90d544..d92ba064e9ef4d047cc6587f9e808c410fb36bd9 100644 --- a/lib/ui/contacts_cache.dart +++ b/lib/ui/contacts_cache.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:html'; @@ -7,34 +8,6 @@ import '../data/models/contact.dart'; import '../g1/api.dart'; import 'logger.dart'; -class ContactsCacheBasic { - factory ContactsCacheBasic() { - _instance ??= ContactsCacheBasic._internal(); - return _instance!; - } - - ContactsCacheBasic._internal(); - - static ContactsCacheBasic? _instance; - - final Map<String, Contact> _cache = <String, Contact>{}; - - Future<Contact> getContact(String pubKey) async { - final String cacheKey = 'avatar-$pubKey'; - - final Contact? cachedContact = _cache[cacheKey]; - if (cachedContact != null) { - return cachedContact; - } - - final Contact contact = await getProfile(pubKey); - - _cache[cacheKey] = contact; - - return contact; - } -} - class ContactsCache { factory ContactsCache() { _instance ??= ContactsCache._internal(); @@ -44,6 +17,7 @@ class ContactsCache { ContactsCache._internal(); static ContactsCache? _instance; + final Map<String, List<Completer<Contact>>> _pendingRequests = {}; Future<Contact> getContact(String pubKey) async { final String cacheKey = 'avatar-$pubKey'; @@ -53,13 +27,13 @@ class ContactsCache { final String? cachedValue = window.localStorage[cacheKey]; if (cachedValue != null) { final Map<String, dynamic> decodedValue = - json.decode(cachedValue) as Map<String, dynamic>; + json.decode(cachedValue) as Map<String, dynamic>; final DateTime timestamp = - DateTime.parse(decodedValue['timestamp'] as String); + DateTime.parse(decodedValue['timestamp'] as String); if (DateTime.now().isBefore(timestamp.add(duration))) { final Contact contact = - Contact.fromJson(decodedValue['data'] as Map<String, dynamic>); + Contact.fromJson(decodedValue['data'] as Map<String, dynamic>); if (!kReleaseMode) { logger('Returning cached contact $contact'); } @@ -70,16 +44,40 @@ class ContactsCache { logger('Error while retrieving contact from cache: $e, $pubKey'); } - final Contact contact = await getProfile(pubKey); + if (_pendingRequests.containsKey(pubKey)) { + final Completer<Contact> completer = Completer<Contact>(); + _pendingRequests[pubKey]!.add(completer); + return completer.future; + } + + final Completer<Contact> completer = Completer<Contact>(); + _pendingRequests[pubKey] = <Completer<Contact>>[completer]; + try { + final Contact contact = await getProfile(pubKey); + + final String encodedValue = json.encode(<String, dynamic>{ + 'timestamp': DateTime.now().toIso8601String(), + 'data': contact.toJson(), + }); + window.localStorage[cacheKey] = encodedValue; + if (!kReleaseMode) { + logger('Returning non cached contact $contact'); + } + // Send to listeners + for (final Completer<Contact> completer in _pendingRequests[pubKey]!) { + completer.complete(contact); + } + _pendingRequests.remove(pubKey); + + return contact; + } catch (e) { + // Send error to listeners + for (final Completer<Contact> completer in _pendingRequests[pubKey]!) { + completer.completeError(e); + } + _pendingRequests.remove(pubKey); - final String encodedValue = json.encode(<String, dynamic>{ - 'timestamp': DateTime.now().toIso8601String(), - 'data': contact.toJson(), - }); - window.localStorage[cacheKey] = encodedValue; - if (!kReleaseMode) { - logger('Returning non cached contact $contact'); + rethrow; } - return contact; } } diff --git a/lib/ui/ui_helpers.dart b/lib/ui/ui_helpers.dart index c96580962f902b2176d555955b2b41602cdbf4c9..0115eda8aae525103e6e540f831f7d4607c4cad1 100644 --- a/lib/ui/ui_helpers.dart +++ b/lib/ui/ui_helpers.dart @@ -124,7 +124,7 @@ Contact contactFromResultSearch(Map<String, dynamic> record) { final Map<String, dynamic> source = record['_source'] as Map<String, dynamic>; final Uint8List? avatarBase64 = _getAvatarFromResults(source); return Contact( - pubkey: record['_id'] as String, + pubKey: record['_id'] as String, name: source['title'] as String, avatar: avatarBase64); } @@ -132,7 +132,7 @@ Contact contactFromResultSearch(Map<String, dynamic> record) { Contact contactFromUserProfile(Map<String, dynamic> source) { final Uint8List? avatarBase64 = _getAvatarFromResults(source); return Contact( - pubkey: source['issuer'] as String, + pubKey: source['issuer'] as String, name: source['title'] as String, avatar: avatarBase64); } diff --git a/lib/ui/widgets/first_screen/pay_contact_search_page.dart b/lib/ui/widgets/first_screen/pay_contact_search_page.dart index 9f32790af25ac2c3a6b9f6a3a364e1a7f5bc69bb..3b4e83876e44c038e7584e07a9d608f83b63c877 100644 --- a/lib/ui/widgets/first_screen/pay_contact_search_page.dart +++ b/lib/ui/widgets/first_screen/pay_contact_search_page.dart @@ -66,7 +66,7 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> { logger('$_searchTerm looks like a plain pub key'); setState(() { _isLoading = true; - final Contact contact = Contact(pubkey: _searchTerm); + final Contact contact = Contact(pubKey: _searchTerm); _results.add(contact); _isLoading = false; }); @@ -107,7 +107,7 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> { if (_results.length == 1 && pay != null) { final Contact contact = _results[0]; paymentCubit.selectUser( - contact.pubkey, + contact.pubKey, contact.nick ?? contact.name, contact.avatar, pay.amount); @@ -191,7 +191,7 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> { Widget _buildItem(Contact contact, int index, BuildContext context) { logger('Contact retrieved $contact'); - final String pubKey = contact.pubkey; + final String pubKey = contact.pubKey; final String title = contact.nick ?? contact.name ?? humanizePubKey(pubKey); final Widget? subtitle = (contact.nick != null || contact.name != null) ? Text(humanizePubKey(pubKey)) @@ -226,7 +226,7 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> { contactsCubit.addContact(contact); } else { contactsCubit.removeContact(Contact( - pubkey: pubKey, + pubKey: pubKey, )); } }); diff --git a/lib/ui/widgets/fourth_screen/transaction_item.dart b/lib/ui/widgets/fourth_screen/transaction_item.dart index 4d6f23cf561bf14d86f375dde44e97a9fcb8e48b..8bd13dfc751545d86cef6640d1a3160128deded6 100644 --- a/lib/ui/widgets/fourth_screen/transaction_item.dart +++ b/lib/ui/widgets/fourth_screen/transaction_item.dart @@ -194,7 +194,7 @@ class TransactionListItem extends StatelessWidget { })); Future<Contact> _fetchContact(String pubKey, Transaction transaction) async { - return Contact(pubkey: pubKey); + return Contact(pubKey: pubKey); if (pubKey == transaction.from) { return ContactsCache().getContact(transaction.to); } else { diff --git a/lib/ui/widgets/third_screen/contacts_page.dart b/lib/ui/widgets/third_screen/contacts_page.dart index 19c0bdd892f7932446dc489bb02830e48ee74d6f..be8cf150fee2ba325886e30f546a8102bacbf723 100644 --- a/lib/ui/widgets/third_screen/contacts_page.dart +++ b/lib/ui/widgets/third_screen/contacts_page.dart @@ -104,7 +104,7 @@ class _ContactsPageState extends State<ContactsPage> { children: <SlidableAction>[ SlidableAction( onPressed: (BuildContext c) { - FlutterClipboard.copy(contact.pubkey).then( + FlutterClipboard.copy(contact.pubKey).then( (dynamic value) => ScaffoldMessenger.of( context) .showSnackBar(SnackBar( @@ -132,9 +132,9 @@ class _ContactsPageState extends State<ContactsPage> { ? Text(contact.nick!) : contact.name != null ? Text(contact.name!) - : humanizePubKeyAsWidget(contact.pubkey), + : humanizePubKeyAsWidget(contact.pubKey), subtitle: contact.nick != null || contact.name != null - ? humanizePubKeyAsWidget(contact.pubkey) + ? humanizePubKeyAsWidget(contact.pubKey) : null, leading: avatar( contact.avatar, @@ -155,7 +155,7 @@ class _ContactsPageState extends State<ContactsPage> { void onSent(BuildContext c, Contact contact) { c .read<PaymentCubit>() - .selectUser(contact.pubkey, contact.nick, contact.avatar); + .selectUser(contact.pubKey, contact.nick, contact.avatar); c.read<BottomNavCubit>().updateIndex(0); } }