diff --git a/lib/features/caching/config/hive_cache_config.dart b/lib/features/caching/config/hive_cache_config.dart index 3d45d73e7..dee33e3da 100644 --- a/lib/features/caching/config/hive_cache_config.dart +++ b/lib/features/caching/config/hive_cache_config.dart @@ -6,6 +6,8 @@ import 'package:tmail_ui_user/features/mailbox/data/model/mailbox_cache.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/mailbox_rights_cache.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/state_cache.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/state_type.dart'; +import 'package:tmail_ui_user/features/thread/data/model/email_address_hive_cache.dart'; +import 'package:tmail_ui_user/features/thread/data/model/email_cache.dart'; class HiveCacheConfig { @@ -29,5 +31,7 @@ class HiveCacheConfig { Hive.registerAdapter(MailboxRightsCacheAdapter()); Hive.registerAdapter(StateCacheAdapter()); Hive.registerAdapter(StateTypeAdapter()); + Hive.registerAdapter(EmailAddressHiveCacheAdapter()); + Hive.registerAdapter(EmailCacheAdapter()); } } \ No newline at end of file diff --git a/lib/features/caching/email_cache_client.dart b/lib/features/caching/email_cache_client.dart new file mode 100644 index 000000000..ce06094c3 --- /dev/null +++ b/lib/features/caching/email_cache_client.dart @@ -0,0 +1,121 @@ + +import 'package:hive/hive.dart'; +import 'package:tmail_ui_user/features/caching/config/hive_cache_client.dart'; +import 'package:tmail_ui_user/features/thread/data/model/email_cache.dart'; + +class EmailCacheClient extends HiveCacheClient { + + @override + String get tableName => 'EmailCache'; + + @override + Future> openBox() { + return Future.sync(() async { + if (Hive.isBoxOpen(tableName)) { + return Hive.box(tableName); + } + return await Hive.openBox(tableName); + }).catchError((error) { + throw error; + }); + } + + @override + Future isExistItem(String key) { + return Future.sync(() async { + final boxEmail = await openBox(); + return boxEmail.containsKey(key); + }).catchError((error) { + throw error; + }); + } + + @override + Future deleteItem(String key) { + return Future.sync(() async { + final boxEmail = await openBox(); + return boxEmail.delete(key); + }).catchError((error) { + throw error; + }); + } + + @override + Future getItem(String key) { + return Future.sync(() async { + final boxEmail = await openBox(); + return boxEmail.get(key); + }).catchError((error) { + throw error; + }); + } + + @override + Future> getAll() { + return Future.sync(() async { + final boxEmail = await openBox(); + return boxEmail.values.toList(); + }).catchError((error) { + throw error; + }); + } + + @override + Future insertItem(String key, EmailCache newObject) { + return Future.sync(() async { + final boxEmail = await openBox(); + boxEmail.put(key, newObject); + }).catchError((error) { + throw error; + }); + } + + @override + Future insertMultipleItem(Map mapObject) { + return Future.sync(() async { + final boxEmail = await openBox(); + boxEmail.putAll(mapObject); + }).catchError((error) { + throw error; + }); + } + + @override + Future updateItem(String key, EmailCache newObject) { + return Future.sync(() async { + final boxEmail = await openBox(); + boxEmail.put(key, newObject); + }).catchError((error) { + throw error; + }); + } + + @override + Future isExistTable() { + return Future.sync(() async { + return await Hive.boxExists(tableName); + }).catchError((error) { + throw error; + }); + } + + @override + Future deleteMultipleItem(List listKey) { + return Future.sync(() async { + final boxEmail = await openBox(); + return boxEmail.deleteAll(listKey); + }).catchError((error) { + throw error; + }); + } + + @override + Future updateMultipleItem(Map mapObject) { + return Future.sync(() async { + final boxEmail = await openBox(); + boxEmail.putAll(mapObject); + }).catchError((error) { + throw error; + }); + } +} \ No newline at end of file diff --git a/lib/features/thread/data/extensions/email_address_extension.dart b/lib/features/thread/data/extensions/email_address_extension.dart new file mode 100644 index 000000000..24c19a1e7 --- /dev/null +++ b/lib/features/thread/data/extensions/email_address_extension.dart @@ -0,0 +1,8 @@ + +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:tmail_ui_user/features/thread/data/model/email_address_hive_cache.dart'; + +extension EmailAddressExtension on EmailAddress { + + EmailAddressHiveCache toEmailAddressHiveCache() => EmailAddressHiveCache(name, email); +} \ No newline at end of file diff --git a/lib/features/thread/data/extensions/email_extension.dart b/lib/features/thread/data/extensions/email_extension.dart new file mode 100644 index 000000000..0ac555cfe --- /dev/null +++ b/lib/features/thread/data/extensions/email_extension.dart @@ -0,0 +1,25 @@ +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/thread/data/model/email_cache.dart'; +import 'package:tmail_ui_user/features/thread/data/extensions/map_keywords_extension.dart'; +import 'package:tmail_ui_user/features/thread/data/extensions/email_address_extension.dart'; + +extension EmailExtension on Email { + + EmailCache toEmailCache() { + return EmailCache( + id.toString(), + keywords: keywords?.toMapString(), + size: size?.value.round(), + receivedAt: receivedAt?.value, + hasAttachment: hasAttachment, + preview: preview, + subject: subject, + sentAt: sentAt?.value, + from: from?.map((emailAddress) => emailAddress.toEmailAddressHiveCache()).toList(), + to: to?.map((emailAddress) => emailAddress.toEmailAddressHiveCache()).toList(), + cc: cc?.map((emailAddress) => emailAddress.toEmailAddressHiveCache()).toList(), + bcc: bcc?.map((emailAddress) => emailAddress.toEmailAddressHiveCache()).toList(), + replyTo: replyTo?.map((emailAddress) => emailAddress.toEmailAddressHiveCache()).toList(), + ); + } +} \ No newline at end of file diff --git a/lib/features/thread/data/extensions/map_keywords_extension.dart b/lib/features/thread/data/extensions/map_keywords_extension.dart new file mode 100644 index 000000000..3b6598c0a --- /dev/null +++ b/lib/features/thread/data/extensions/map_keywords_extension.dart @@ -0,0 +1,7 @@ + +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; + +extension MapKeywordsExtension on Map { + + Map toMapString() => Map.fromIterables(keys.map((keyword) => keyword.value), values); +} \ No newline at end of file diff --git a/lib/features/thread/data/model/email_address_hive_cache.g.dart b/lib/features/thread/data/model/email_address_hive_cache.g.dart new file mode 100644 index 000000000..e2fb40100 --- /dev/null +++ b/lib/features/thread/data/model/email_address_hive_cache.g.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'email_address_hive_cache.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class EmailAddressHiveCacheAdapter extends TypeAdapter { + @override + final int typeId = 6; + + @override + EmailAddressHiveCache read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return EmailAddressHiveCache( + fields[0] as String?, + fields[1] as String?, + ); + } + + @override + void write(BinaryWriter writer, EmailAddressHiveCache obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(obj.name) + ..writeByte(1) + ..write(obj.email); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is EmailAddressHiveCacheAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/features/thread/data/model/email_cache.g.dart b/lib/features/thread/data/model/email_cache.g.dart new file mode 100644 index 000000000..9a214147b --- /dev/null +++ b/lib/features/thread/data/model/email_cache.g.dart @@ -0,0 +1,77 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'email_cache.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class EmailCacheAdapter extends TypeAdapter { + @override + final int typeId = 5; + + @override + EmailCache read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return EmailCache( + fields[0] as String, + keywords: (fields[1] as Map?)?.cast(), + size: fields[2] as int?, + receivedAt: fields[3] as DateTime?, + hasAttachment: fields[4] as bool?, + preview: fields[5] as String?, + subject: fields[6] as String?, + sentAt: fields[7] as DateTime?, + from: (fields[8] as List?)?.cast(), + to: (fields[9] as List?)?.cast(), + cc: (fields[10] as List?)?.cast(), + bcc: (fields[11] as List?)?.cast(), + replyTo: (fields[12] as List?)?.cast(), + ); + } + + @override + void write(BinaryWriter writer, EmailCache obj) { + writer + ..writeByte(13) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.keywords) + ..writeByte(2) + ..write(obj.size) + ..writeByte(3) + ..write(obj.receivedAt) + ..writeByte(4) + ..write(obj.hasAttachment) + ..writeByte(5) + ..write(obj.preview) + ..writeByte(6) + ..write(obj.subject) + ..writeByte(7) + ..write(obj.sentAt) + ..writeByte(8) + ..write(obj.from) + ..writeByte(9) + ..write(obj.to) + ..writeByte(10) + ..write(obj.cc) + ..writeByte(11) + ..write(obj.bcc) + ..writeByte(12) + ..write(obj.replyTo); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is EmailCacheAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/test/features/caching/email_cache_client_test.dart b/test/features/caching/email_cache_client_test.dart new file mode 100644 index 000000000..c3c312dba --- /dev/null +++ b/test/features/caching/email_cache_client_test.dart @@ -0,0 +1,181 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; +import 'package:tmail_ui_user/features/caching/email_cache_client.dart'; +import 'package:tmail_ui_user/features/thread/data/extensions/email_extension.dart'; + +import '../../fixtures/email_fixtures.dart'; + +void main() { + + late EmailCacheClient _emailCacheClient; + + setUpAll(() { + HiveCacheConfig().setUp(cachePath: Directory.current.path); + }); + + setUp(() { + _emailCacheClient = EmailCacheClient(); + }); + + group('[delete]', () { + test('cache should delete item successfully when cache empty', () async { + await _emailCacheClient.deleteItem(EmailFixtures.email1.id.toString()); + + final remainingItems = await _emailCacheClient.getAll(); + + expect(remainingItems.length, equals(0)); + }); + + test('cache should not delete item which not in the list', () async { + await _emailCacheClient.insertItem( + EmailFixtures.email1.id.toString(), + EmailFixtures.email1.toEmailCache()); + + await _emailCacheClient.insertItem( + EmailFixtures.email2.id.toString(), + EmailFixtures.email2.toEmailCache()); + + await _emailCacheClient.deleteItem(EmailFixtures.email3.id.toString()); + + final remainingItems = await _emailCacheClient.getAll(); + + expect(remainingItems.length, equals(2)); + expect( + remainingItems, + containsAll({ + EmailFixtures.email1.toEmailCache(), + EmailFixtures.email2.toEmailCache() + })); + }); + + test('cache should delete item successfully', () async { + await _emailCacheClient.insertItem( + EmailFixtures.email1.id.toString(), + EmailFixtures.email1.toEmailCache()); + + await _emailCacheClient.insertItem( + EmailFixtures.email2.id.toString(), + EmailFixtures.email2.toEmailCache()); + + await _emailCacheClient.deleteItem(EmailFixtures.email1.id.toString()); + + final remainingItems = await _emailCacheClient.getAll(); + + expect(remainingItems.length, equals(1)); + expect(remainingItems.first, equals(EmailFixtures.email2.toEmailCache())); + }); + + test('cache should not delete item twice', () async { + await _emailCacheClient.insertItem( + EmailFixtures.email1.id.toString(), + EmailFixtures.email1.toEmailCache()); + + await _emailCacheClient.insertItem( + EmailFixtures.email2.id.toString(), + EmailFixtures.email2.toEmailCache()); + + await _emailCacheClient.deleteItem(EmailFixtures.email1.id.toString()); + await _emailCacheClient.deleteItem(EmailFixtures.email1.id.toString()); + + final remainingItems = await _emailCacheClient.getAll(); + + expect(remainingItems.length, equals(1)); + expect(remainingItems.first, equals(EmailFixtures.email2.toEmailCache())); + }); + }); + + group('[add]', () { + test('cache should add item when cache empty', () async { + await _emailCacheClient.insertItem( + EmailFixtures.email1.id.toString(), + EmailFixtures.email1.toEmailCache()); + + final remainingItems = await _emailCacheClient.getAll(); + + expect(remainingItems.length, equals(1)); + expect(remainingItems.first, equals(EmailFixtures.email1.toEmailCache())); + }); + + test('cache should add item when cache not empty', () async { + await _emailCacheClient.insertItem( + EmailFixtures.email1.id.toString(), + EmailFixtures.email1.toEmailCache()); + + await _emailCacheClient.insertItem( + EmailFixtures.email2.id.toString(), + EmailFixtures.email2.toEmailCache()); + + final remainingItems = await _emailCacheClient.getAll(); + + expect(remainingItems.length, equals(2)); + expect( + remainingItems, + containsAll({ + EmailFixtures.email1.toEmailCache(), + EmailFixtures.email2.toEmailCache() + })); + }); + + test('cache should not add item twice', () async { + await _emailCacheClient.insertItem( + EmailFixtures.email1.id.toString(), + EmailFixtures.email1.toEmailCache()); + + await _emailCacheClient.insertItem( + EmailFixtures.email1.id.toString(), + EmailFixtures.email1.toEmailCache()); + + final remainingItems = await _emailCacheClient.getAll(); + + expect(remainingItems.length, equals(1)); + expect( + remainingItems, + containsAll({ + EmailFixtures.email1.toEmailCache(), + })); + }); + }); + + group('[update]', () { + test('cache should update item when update to iem which not in cache', () async { + await _emailCacheClient.updateItem( + EmailFixtures.email1.id.toString(), + EmailFixtures.email1.toEmailCache()); + + final remainingItems = await _emailCacheClient.getAll(); + + expect(remainingItems.length, equals(1)); + expect(remainingItems.first, equals(EmailFixtures.email1.toEmailCache())); + }); + + test('cache should update correctly item', () async { + await _emailCacheClient.insertItem( + EmailFixtures.email1.id.toString(), + EmailFixtures.email1.toEmailCache()); + + await _emailCacheClient.insertItem( + EmailFixtures.email2.id.toString(), + EmailFixtures.email2.toEmailCache()); + + await _emailCacheClient.updateItem( + EmailFixtures.email1.id.toString(), + EmailFixtures.email3.toEmailCache()); + + final remainingItems = await _emailCacheClient.getAll(); + + expect(remainingItems.length, equals(2)); + expect( + remainingItems, + containsAll({ + EmailFixtures.email2.toEmailCache(), + EmailFixtures.email3.toEmailCache() + })); + }); + }); + + tearDown(() async { + await _emailCacheClient.deleteBox(); + }); +} \ No newline at end of file diff --git a/test/fixtures/email_fixtures.dart b/test/fixtures/email_fixtures.dart new file mode 100644 index 000000000..b2f4b21af --- /dev/null +++ b/test/fixtures/email_fixtures.dart @@ -0,0 +1,66 @@ +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/utc_date.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; + +class EmailFixtures { + static final email1 = Email( + EmailId(Id("382312d0-fa5c-11eb-b647-2fef1ee78d9e")), + preview: "Dear QA,I attached image here", + hasAttachment: false, + subject: "test inline image", + from: {EmailAddress("DatPH", "dphamhoang@linagora.com")}, + sentAt: UTCDate(DateTime.parse("2021-08-11T04:25:34Z")), + receivedAt: UTCDate(DateTime.parse("2021-08-11T04:25:55Z")), + keywords: {KeyWordIdentifier.emailSeen : true}, + to: {EmailAddress("DatVu", "tdvu@linagora.com")}, + ); + + static final email2 = Email( + EmailId(Id("bc8a5320-fa58-11eb-b647-2fef1ee78d9e")), + preview: "This event is about to begin Noti check TimeFriday 23 October 2020 12:00 - 12:30 Europe/Paris (See in Calendar)Location1 thai ha (See in Map)Attendees - User A (Organizer) - Lê Nguyễn - User C (Organizer) - User A Resourc", + hasAttachment: false, + subject: "Notification: Recurrencr", + from: {EmailAddress(null, "noreply@qa.open-paas.org")}, + sentAt: UTCDate(DateTime.parse("2021-08-11T03:00:00Z")), + receivedAt: UTCDate(DateTime.parse("2021-08-11T04:00:55Z")), + keywords: {KeyWordIdentifier.emailFlagged : true}, + to: {EmailAddress("DatVu", "tdvu@gmail.com")}, + ); + + static final email4 = Email( + EmailId(Id("d9b3b880-fa6f-11eb-b647-2fef1ee78d9e")), + preview: "alo -- desktop signature", + hasAttachment: true, + subject: "test attachment", + from: {EmailAddress("Haaheoo", "userc@qa.open-paas.org")}, + sentAt: UTCDate(DateTime.parse("2021-08-11T06:46:25Z")), + receivedAt: UTCDate(DateTime.parse("2021-08-11T06:46:26Z")), + keywords: {KeyWordIdentifier.emailFlagged : true}, + to: {EmailAddress("DatVu", "tdvu@icloud.com")}, + ); + + static final email5 = Email( + EmailId(Id("637f1ef0-fa5d-11eb-b647-2fef1ee78d9e")), + preview: "Dear, test inline Thanks and BRs-- desktop signature", + hasAttachment: false, + subject: "test inline image", + from: {EmailAddress("Haaheoo", "userc@qa.open-paas.org")}, + sentAt: UTCDate(DateTime.parse("2021-08-11T04:34:13Z")), + receivedAt: UTCDate(DateTime.parse("2021-08-11T04:34:17Z")), + keywords: {KeyWordIdentifier.emailSeen : true}, + to: {EmailAddress("DatVu", "tdvu@yahoo.com")}, + ); +} \ No newline at end of file