import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import '../data/models/contact.dart'; import '../g1/api.dart'; import '../g1/g1_helper.dart'; import 'logger.dart'; class ContactsCache { factory ContactsCache() { _instance ??= ContactsCache._internal(); return _instance!; } ContactsCache._internal(); Box<dynamic>? box; Future<void> init([bool test = false]) async { if (test) { box = MemoryFallbackBox<Map<String, dynamic>>(); return; } try { if (kIsWeb) { box = await Hive.openBox(_boxName); } else { final Directory appDocDir = await getApplicationDocumentsDirectory(); final String appDocPath = appDocDir.path; box = await Hive.openBox(_boxName, path: appDocPath); } // We clear the box on every startup to avoid issues with old data } catch (e) { logger('Error opening Hive: $e'); } box ??= MemoryFallbackBox<Map<String, dynamic>>(); } Future<void> addContacts(List<Contact> contacts) async { for (final Contact contact in contacts) { await addContact(contact); } } Future<void> dispose() async { await box?.close(); } Future<void> clear() async { await box?.clear(); } static ContactsCache? _instance; final Map<String, List<Completer<Contact>>> _pendingRequests = <String, List<Completer<Contact>>>{}; final String _boxName = 'contacts_cache'; Contact? getCachedContact(String pubKey, [bool debug = false, bool withoutAvatar = false]) { return withoutAvatar ? _retrieveContact(pubKey)?.cloneWithoutAvatar() : _retrieveContact(pubKey); } Future<Contact> getContact(String pubKey, [bool debug = false]) async { Contact? cachedContact; try { cachedContact = _retrieveContact(pubKey); } catch (e, stackTrace) { await Sentry.captureException(e, stackTrace: stackTrace); logger('Error while retrieving contact from cache: $e, $pubKey'); } if (cachedContact != null) { if (!kReleaseMode && debug) { logger('Returning cached contact $cachedContact'); } return cachedContact; } else { if (!kReleaseMode && debug) { logger('Contact $pubKey not cached'); } } 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 { cachedContact = await getProfile(pubKey); _storeContact(cachedContact); if (!kReleaseMode && debug) { logger('Returning non cached contact $cachedContact'); } // Send to listeners for (final Completer<Contact> completer in _pendingRequests[pubKey]!) { completer.complete(cachedContact); } _pendingRequests.remove(pubKey); return cachedContact; } catch (e, stackTrace) { // Send error to listeners for (final Completer<Contact> completer in _pendingRequests[pubKey]!) { completer.completeError(e); } _pendingRequests.remove(pubKey); await Sentry.captureException(e, stackTrace: stackTrace); rethrow; } } Future<void> saveContact(Contact contact) async => addContact(contact); Future<void> addContact(Contact contactRaw) async { // Get the cached version of the contact, if it exists final Contact contact = contactRaw.copyWith(pubKey: extractPublicKey(contactRaw.pubKey)); Contact? cachedContact = _retrieveContact(contact.pubKey); // Merge the new contact with the cached contact if (cachedContact != null) { // logger('Merging contact $contact with cached contact $cachedContact'); cachedContact = cachedContact.merge(contact); } else { // logger('Adding contact $contact to cache as is not cached'); cachedContact = contact; } // Cache the merged contact // Cache the merged contact await _storeContact(cachedContact); // logger('Added contact $cachedContact to cache'); } Future<void> _storeContact(Contact contact) async { await box!.put(contact.pubKey, <String, dynamic>{ 'timestamp': DateTime.now().toIso8601String(), 'data': json.encode(contact.toJson()), }); } Contact? _retrieveContact(String pubKey) { final dynamic record = box!.get(pubKey); if (record != null) { final Map<String, dynamic> typedRecord = Map<String, dynamic>.from(record as Map<dynamic, dynamic>); // final DateTime timestamp = // DateTime.parse(typedRecord['timestamp'] as String); final Contact contact = Contact.fromJson( json.decode(typedRecord['data'] as String) as Map<String, dynamic>); return contact; } return null; } } class MemoryFallbackBox<E> extends Box<E> { final Map<String, dynamic> _storage = HashMap<String, dynamic>(); @override String get name => '_memory_fallback_box'; @override bool get isOpen => true; @override String? get path => null; @override bool get lazy => false; @override Iterable<dynamic> get keys => _storage.keys; @override int get length => _storage.length; @override bool get isEmpty => _storage.isEmpty; @override bool get isNotEmpty => _storage.isNotEmpty; @override dynamic keyAt(int index) { return _storage.keys.elementAt(index); } @override Stream<BoxEvent> watch({dynamic key}) { throw UnimplementedError('watch() is not supported in _MemoryFallbackBox'); } @override bool containsKey(dynamic key) { return _storage.containsKey(key); } @override Future<void> put(dynamic key, dynamic value) async { _storage[key as String] = value; } @override Future<void> putAt(int index, dynamic value) async { _storage[_storage.keys.elementAt(index)] = value; } @override Future<void> putAll(Map<dynamic, dynamic> entries) async { _storage.addAll(entries as Map<String, dynamic>); } @override Future<int> add(E value) async { throw UnimplementedError('add() is not supported in _MemoryFallbackBox'); } @override Future<Iterable<int>> addAll(Iterable<E> values) async { throw UnimplementedError('addAll() is not supported in _MemoryFallbackBox'); } @override Future<void> delete(dynamic key) async { _storage.remove(key); } @override Future<void> deleteAt(int index) async { _storage.remove(_storage.keys.elementAt(index)); } @override Future<void> deleteAll(Iterable<dynamic> keys) async { // ignore: prefer_foreach for (final dynamic key in keys) { _storage.remove(key); } } @override Future<void> compact() async {} @override Future<int> clear() async { final int count = _storage.length; _storage.clear(); return count; } @override Future<void> close() async {} @override Future<void> deleteFromDisk() async {} @override Future<void> flush() async {} @override E? get(dynamic key, {E? defaultValue}) { return _storage.containsKey(key) ? _storage[key] as E : defaultValue; } @override E? getAt(int index) { return _storage.values.elementAt(index) as E?; } @override Map<dynamic, E> toMap() { return Map<dynamic, E>.from(_storage); } @override Iterable<E> get values => _storage.values.cast<E>(); @override Iterable<E> valuesBetween({dynamic startKey, dynamic endKey}) { if (startKey == null && endKey == null) { return values; } final int startIndex = startKey != null ? _storage.keys.toList().indexOf(startKey as String) : 0; final int endIndex = endKey != null ? _storage.keys.toList().indexOf(endKey as String) : _storage.length - 1; if (startIndex < 0 || endIndex < 0) { throw ArgumentError('Start key or end key not found in the box.'); } return _storage.values .skip(startIndex) .take(endIndex - startIndex + 1) .cast<E>(); } }