Skip to content
Snippets Groups Projects
Commit 8d01b6ce authored by vjrj's avatar vjrj
Browse files

Improved contact cache. Contact serialization fix

parent 05ac9e50
No related branches found
No related tags found
No related merge requests found
......@@ -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';
}
}
......@@ -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,
};
......@@ -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);
}
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>[];
......@@ -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']) ??
......
......@@ -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?,
);
......
......@@ -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) {
......
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;
}
}
......@@ -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);
}
......
......@@ -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,
));
}
});
......
......@@ -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 {
......
......@@ -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);
}
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment