diff --git a/lib/src/database/daos/contacts.dao.dart b/lib/src/database/daos/contacts.dao.dart index 0fbfbaf..8e91080 100644 --- a/lib/src/database/daos/contacts.dao.dart +++ b/lib/src/database/daos/contacts.dao.dart @@ -99,7 +99,7 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { .watchSingleOrNull(); } - Future> getAllNotBlockedContacts() { + Future> getAllContacts() { return select(contacts).get(); } diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 1c9a5ae..b76063c 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -3,6 +3,7 @@ import 'package:hashlib/random.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -278,89 +279,6 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { return query.map((row) => row.readTable(groups)).getSingleOrNull(); } - Future incFlameCounter( - String groupId, - bool received, - DateTime timestamp, - ) async { - final group = await (select(groups) - ..where((t) => t.groupId.equals(groupId))) - .getSingle(); - - final totalMediaCounter = group.totalMediaCounter + 1; - var flameCounter = group.flameCounter; - var maxFlameCounter = group.maxFlameCounter; - var maxFlameCounterFrom = group.maxFlameCounterFrom; - - if (group.lastMessageReceived != null && group.lastMessageSend != null) { - final now = DateTime.now(); - final startOfToday = DateTime(now.year, now.month, now.day); - final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); - if (group.lastMessageSend!.isBefore(twoDaysAgo) || - group.lastMessageReceived!.isBefore(twoDaysAgo)) { - flameCounter = 0; - } - } - - var lastMessageSend = const Value.absent(); - var lastMessageReceived = const Value.absent(); - var lastFlameCounterChange = const Value.absent(); - - if (group.lastFlameCounterChange != null) { - final now = DateTime.now(); - final startOfToday = DateTime(now.year, now.month, now.day); - - if (group.lastFlameCounterChange!.isBefore(startOfToday)) { - // last flame update was yesterday. check if it can be updated. - var updateFlame = false; - if (received) { - if (group.lastMessageSend != null && - group.lastMessageSend!.isAfter(startOfToday)) { - // today a message was already send -> update flame - updateFlame = true; - } - } else if (group.lastMessageReceived != null && - group.lastMessageReceived!.isAfter(startOfToday)) { - // today a message was already received -> update flame - updateFlame = true; - } - if (updateFlame) { - flameCounter += 1; - lastFlameCounterChange = Value(timestamp); - // Overwrite max flame counter either the current is bigger or the th max flame counter is older then 4 days - if (flameCounter >= maxFlameCounter || - maxFlameCounterFrom == null || - maxFlameCounterFrom - .isBefore(DateTime.now().subtract(const Duration(days: 5)))) { - maxFlameCounter = flameCounter; - maxFlameCounterFrom = DateTime.now(); - } - } - } - } else { - // There where no message until no... - lastFlameCounterChange = Value(timestamp); - } - - if (received) { - lastMessageReceived = Value(timestamp); - } else { - lastMessageSend = Value(timestamp); - } - - await (update(groups)..where((t) => t.groupId.equals(groupId))).write( - GroupsCompanion( - totalMediaCounter: Value(totalMediaCounter), - lastFlameCounterChange: lastFlameCounterChange, - lastMessageReceived: lastMessageReceived, - lastMessageSend: lastMessageSend, - flameCounter: Value(flameCounter), - maxFlameCounter: Value(maxFlameCounter), - maxFlameCounterFrom: Value(maxFlameCounterFrom), - ), - ); - } - Stream watchSumTotalMediaCounter() { final query = selectOnly(groups) ..addColumns([groups.totalMediaCounter.sum()]); @@ -383,23 +301,3 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { .write(GroupsCompanion(lastMessageExchange: Value(newLastMessage))); } } - -int getFlameCounterFromGroup(Group? group) { - if (group == null) return 0; - if (group.lastMessageSend == null || - group.lastMessageReceived == null || - group.lastFlameCounterChange == null) { - return 0; - } - final now = DateTime.now(); - final startOfToday = DateTime(now.year, now.month, now.day); - final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); - final oneDayAgo = startOfToday.subtract(const Duration(days: 1)); - if (group.lastMessageSend!.isAfter(twoDaysAgo) && - group.lastMessageReceived!.isAfter(twoDaysAgo) || - group.lastFlameCounterChange!.isAfter(oneDayAgo)) { - return group.flameCounter + 1; - } else { - return 0; - } -} diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 7269054..998e51f 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:hashlib/random.dart'; import 'package:twonly/globals.dart'; @@ -110,11 +111,11 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { final allGroups = await select(groups).get(); for (final group in allGroups) { - final deletionTime = DateTime.now().subtract( - Duration( - milliseconds: group.deleteMessagesAfterMilliseconds, - ), - ); + final deletionTime = clock.now().subtract( + Duration( + milliseconds: group.deleteMessagesAfterMilliseconds, + ), + ); await (delete(messages) ..where( (m) => @@ -150,7 +151,7 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { // t.messageOtherId.isNull() & // t.errorWhileSending.equals(false) & // t.sendAt.isBiggerThanValue( - // DateTime.now().subtract(const Duration(minutes: 10)), + // clock.now().subtract(const Duration(minutes: 10)), // ), // )) // .get(); @@ -178,7 +179,7 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { // } Future openedAllTextMessages(String groupId) { - final updates = MessagesCompanion(openedAt: Value(DateTime.now())); + final updates = MessagesCompanion(openedAt: Value(clock.now())); return (update(messages) ..where( (t) => @@ -272,12 +273,12 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { // Directly show as message opened as soon as one person has opened it final openedByAll = await haveAllMembers(messageId, MessageActionType.openedAt) - ? DateTime.now() + ? clock.now() : null; await twonlyDB.messagesDao.updateMessageId( messageId, MessagesCompanion( - openedAt: Value(DateTime.now()), + openedAt: Value(clock.now()), openedByAll: Value(openedByAll), ), ); @@ -298,7 +299,7 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { ); await twonlyDB.messagesDao.updateMessageId( messageId, - MessagesCompanion(ackByServer: Value(DateTime.now())), + MessagesCompanion(ackByServer: Value(clock.now())), ); } @@ -378,7 +379,7 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { await twonlyDB.groupsDao.updateGroup( message.groupId.value, GroupsCompanion( - lastMessageExchange: Value(DateTime.now()), + lastMessageExchange: Value(clock.now()), archived: const Value(false), deletedContent: const Value(false), ), @@ -389,7 +390,7 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { message.groupId.value, message.senderId.value!, GroupMembersCompanion( - lastMessage: Value(DateTime.now()), + lastMessage: Value(clock.now()), ), ); } diff --git a/lib/src/database/daos/receipts.dao.dart b/lib/src/database/daos/receipts.dao.dart index 5977a1d..397f5d6 100644 --- a/lib/src/database/daos/receipts.dao.dart +++ b/lib/src/database/daos/receipts.dao.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:hashlib/random.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; @@ -81,12 +82,12 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { } Future> getReceiptsForRetransmission() async { - final markedRetriesTime = DateTime.now().subtract( - const Duration( - // give the server time to transmit all messages to the client - seconds: 20, - ), - ); + final markedRetriesTime = clock.now().subtract( + const Duration( + // give the server time to transmit all messages to the client + seconds: 20, + ), + ); return (select(receipts) ..where( (t) => @@ -111,7 +112,7 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { Future markMessagesForRetry(int contactId) async { await (update(receipts)..where((c) => c.contactId.equals(contactId))).write( ReceiptsCompanion( - markForRetry: Value(DateTime.now()), + markForRetry: Value(clock.now()), ), ); } diff --git a/lib/src/database/daos/signal.dao.dart b/lib/src/database/daos/signal.dao.dart index 294e358..b04bc08 100644 --- a/lib/src/database/daos/signal.dao.dart +++ b/lib/src/database/daos/signal.dao.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/signal_contact_prekey.table.dart'; @@ -107,9 +108,9 @@ class SignalDao extends DatabaseAccessor with _$SignalDaoMixin { await (delete(signalContactPreKeys) ..where( (t) => (t.createdAt.isSmallerThanValue( - DateTime.now().subtract( - const Duration(days: 100), - ), + clock.now().subtract( + const Duration(days: 100), + ), )), )) .go(); @@ -117,9 +118,9 @@ class SignalDao extends DatabaseAccessor with _$SignalDaoMixin { await (delete(twonlyDB.signalPreKeyStores) ..where( (t) => (t.createdAt.isSmallerThanValue( - DateTime.now().subtract( - const Duration(days: 365), - ), + clock.now().subtract( + const Duration(days: 365), + ), )), )) .go(); diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index 5d4f1f5..4cfb944 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart' show DriftNativeOptions, driftDatabase; @@ -160,9 +161,9 @@ class TwonlyDB extends _$TwonlyDB { await (delete(signalPreKeyStores) ..where( (t) => (t.createdAt.isSmallerThanValue( - DateTime.now().subtract( - const Duration(days: 25), - ), + clock.now().subtract( + const Duration(days: 25), + ), )), )) .go(); diff --git a/lib/src/database/twonly_database_old.dart b/lib/src/database/twonly_database_old.dart index 75ec3c2..60af3bb 100644 --- a/lib/src/database/twonly_database_old.dart +++ b/lib/src/database/twonly_database_old.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart' show DriftNativeOptions, driftDatabase; @@ -191,9 +192,9 @@ class TwonlyDatabaseOld extends _$TwonlyDatabaseOld { await (delete(signalPreKeyStores) ..where( (t) => (t.createdAt.isSmallerThanValue( - DateTime.now().subtract( - const Duration(days: 25), - ), + clock.now().subtract( + const Duration(days: 25), + ), )), )) .go(); diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 850cc6a..762c522 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'dart:math'; import 'dart:ui' as ui; +import 'package:clock/clock.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; @@ -206,7 +207,7 @@ class ApiService { } Future _waitForResponse(Int64 seq) async { - final startTime = DateTime.now(); + final startTime = clock.now(); const timeout = Duration(seconds: 60); @@ -216,7 +217,7 @@ class ApiService { messagesV0.remove(seq); return tmp; } - if (DateTime.now().difference(startTime) > timeout) { + if (clock.now().difference(startTime) > timeout) { Log.warn('Timeout for message $seq'); return null; } diff --git a/lib/src/services/api/client2client/media.c2c.dart b/lib/src/services/api/client2client/media.c2c.dart index 31e2bed..807706a 100644 --- a/lib/src/services/api/client2client/media.c2c.dart +++ b/lib/src/services/api/client2client/media.c2c.dart @@ -7,6 +7,7 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/utils.dart'; +import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; @@ -114,7 +115,7 @@ Future handleMedia( await twonlyDB.groupsDao .increaseLastMessageExchange(groupId, fromTimestamp(media.timestamp)); Log.info('Inserted a new media message with ID: ${message.messageId}'); - await twonlyDB.groupsDao.incFlameCounter( + await incFlameCounter( message.groupId, true, fromTimestamp(media.timestamp), diff --git a/lib/src/services/api/client2client/pushkeys.c2c.dart b/lib/src/services/api/client2client/pushkeys.c2c.dart index 6e3cb4d..31b6984 100644 --- a/lib/src/services/api/client2client/pushkeys.c2c.dart +++ b/lib/src/services/api/client2client/pushkeys.c2c.dart @@ -1,10 +1,11 @@ import 'dart:async'; +import 'package:clock/clock.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/utils/log.dart'; -DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1)); +DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1)); Future handlePushKey( int contactId, @@ -14,8 +15,8 @@ Future handlePushKey( case EncryptedContent_PushKeys_Type.REQUEST: Log.info('Got a pushkey request from $contactId'); if (lastPushKeyRequest - .isBefore(DateTime.now().subtract(const Duration(seconds: 60)))) { - lastPushKeyRequest = DateTime.now(); + .isBefore(clock.now().subtract(const Duration(seconds: 60)))) { + lastPushKeyRequest = clock.now(); unawaited(setupNotificationWithUsers(forceContact: contactId)); } diff --git a/lib/src/services/api/client2client/reaction.c2c.dart b/lib/src/services/api/client2client/reaction.c2c.dart index 5db2fbd..ffad995 100644 --- a/lib/src/services/api/client2client/reaction.c2c.dart +++ b/lib/src/services/api/client2client/reaction.c2c.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/utils/log.dart'; @@ -17,7 +18,6 @@ Future handleReaction( ); if (!reaction.remove) { - await twonlyDB.groupsDao - .increaseLastMessageExchange(groupId, DateTime.now()); + await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now()); } } diff --git a/lib/src/services/api/client2client/text_message.c2c.dart b/lib/src/services/api/client2client/text_message.c2c.dart index f3444cd..2a9b301 100644 --- a/lib/src/services/api/client2client/text_message.c2c.dart +++ b/lib/src/services/api/client2client/text_message.c2c.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; @@ -26,7 +27,7 @@ Future handleTextMessage( textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null, ), createdAt: Value(fromTimestamp(textMessage.timestamp)), - ackByServer: Value(DateTime.now()), + ackByServer: Value(clock.now()), ), ); await twonlyDB.groupsDao.increaseLastMessageExchange( diff --git a/lib/src/services/api/mediafiles/media_background.service.dart b/lib/src/services/api/mediafiles/media_background.service.dart index 37c31af..27d440b 100644 --- a/lib/src/services/api/mediafiles/media_background.service.dart +++ b/lib/src/services/api/mediafiles/media_background.service.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:background_downloader/background_downloader.dart'; +import 'package:clock/clock.dart'; import 'package:drift/drift.dart' show Value; import 'package:flutter/foundation.dart'; import 'package:twonly/globals.dart'; @@ -92,7 +93,7 @@ Future handleUploadStatusUpdate(TaskStatusUpdate update) async { await twonlyDB.messagesDao.handleMessageAckByServer( contact.contactId, message.messageId, - DateTime.now(), + clock.now(), ); } } diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index ee5e08d..26f3d7e 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:background_downloader/background_downloader.dart'; +import 'package:clock/clock.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; @@ -18,6 +19,7 @@ import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/mediafiles/media_background.service.dart'; import 'package:twonly/src/services/api/messages.dart'; +import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -101,8 +103,7 @@ Future insertMediaFileInMessagesTable( type: const Value(MessageType.media), ), ); - await twonlyDB.groupsDao - .increaseLastMessageExchange(groupId, DateTime.now()); + await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now()); if (message != null) { // de-archive contact when sending a new message await twonlyDB.groupsDao.updateGroup( @@ -207,11 +208,7 @@ Future _createUploadRequest(MediaFileService media) async { await twonlyDB.groupsDao.getGroupNonLeftMembers(message.groupId); if (media.mediaFile.reuploadRequestedBy == null) { - await twonlyDB.groupsDao.incFlameCounter( - message.groupId, - false, - message.createdAt, - ); + await incFlameCounter(message.groupId, false, message.createdAt); } for (final groupMember in groupMembers) { diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index 7955e92..9212316 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; @@ -131,7 +132,7 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ await twonlyDB.messagesDao.handleMessageAckByServer( receipt.contactId, receipt.messageId!, - DateTime.now(), + clock.now(), ); } if (!receipt.contactWillSendsReceipt) { @@ -140,9 +141,9 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ await twonlyDB.receiptsDao.updateReceipt( receiptId, ReceiptsCompanion( - ackByServerAt: Value(DateTime.now()), + ackByServerAt: Value(clock.now()), retryCount: Value(receipt.retryCount + 1), - lastRetry: Value(DateTime.now()), + lastRetry: Value(clock.now()), markForRetry: const Value(null), ), ); @@ -210,7 +211,7 @@ Future sendCipherTextToGroup( }) async { final groupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(groupId); - await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, DateTime.now()); + await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now()); encryptedContent.groupId = groupId; @@ -242,7 +243,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText( contactId: Value(contactId), message: Value(response.writeToBuffer()), messageId: Value(messageId), - ackByServerAt: Value(onlyReturnEncryptedData ? DateTime.now() : null), + ackByServerAt: Value(onlyReturnEncryptedData ? clock.now() : null), ), ); @@ -268,7 +269,7 @@ Future notifyContactAboutOpeningMessage( } Log.info('Opened messages: $messageOtherIds'); - final actionAt = DateTime.now(); + final actionAt = clock.now(); await sendCipherText( contactId, diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index 682d321..c3600f1 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:hashlib/random.dart'; import 'package:mutex/mutex.dart'; @@ -60,7 +61,7 @@ Future handleServerMessage(server.ServerToClient msg) async { await apiService.sendResponse(ClientToServer()..v0 = v0); } -DateTime lastPushKeyRequest = DateTime.now().subtract(const Duration(hours: 1)); +DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1)); Mutex protectReceiptCheck = Mutex(); diff --git a/lib/src/services/flame.service.dart b/lib/src/services/flame.service.dart index 8eff7fb..ff98bd1 100644 --- a/lib/src/services/flame.service.dart +++ b/lib/src/services/flame.service.dart @@ -1,8 +1,8 @@ +import 'package:clock/clock.dart'; import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/database/daos/groups.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/messages.dart'; @@ -32,10 +32,10 @@ Future syncFlameCounters({String? forceForGroup}) async { } } - final flameCounter = getFlameCounterFromGroup(group) - 1; + final flameCounter = getFlameCounterFromGroup(group); - // only sync when flame counter is higher than three days or when they are bestFriends - if (flameCounter < 1 && bestFriend.groupId != group.groupId) continue; + // only sync when flame counter is higher three or when they are bestFriends + if (flameCounter <= 2 && bestFriend.groupId != group.groupId) continue; final groupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId); @@ -59,8 +59,125 @@ Future syncFlameCounters({String? forceForGroup}) async { await twonlyDB.groupsDao.updateGroup( group.groupId, GroupsCompanion( - lastFlameSync: Value(DateTime.now()), + lastFlameSync: Value(clock.now()), ), ); } } + +int getFlameCounterFromGroup(Group? group) { + if (group == null) return 0; + if (group.lastMessageSend == null || + group.lastMessageReceived == null || + group.lastFlameCounterChange == null) { + return 0; + } + final now = clock.now(); + final startOfToday = DateTime(now.year, now.month, now.day); + final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); + final oneDayAgo = startOfToday.subtract(const Duration(days: 1)); + if (group.lastMessageSend!.isAfter(twoDaysAgo) && + group.lastMessageReceived!.isAfter(twoDaysAgo) || + group.lastFlameCounterChange!.isAfter(oneDayAgo)) { + return group.flameCounter; + } else { + return 0; + } +} + +Future incFlameCounter( + String groupId, + bool received, + DateTime timestamp, +) async { + final group = await twonlyDB.groupsDao.getGroup(groupId); + if (group == null) return; + + final totalMediaCounter = group.totalMediaCounter + 1; + var flameCounter = group.flameCounter; + var maxFlameCounter = group.maxFlameCounter; + var maxFlameCounterFrom = group.maxFlameCounterFrom; + + if (group.lastMessageReceived != null && group.lastMessageSend != null) { + final now = clock.now(); + final startOfToday = DateTime(now.year, now.month, now.day); + final twoDaysAgo = startOfToday.subtract(const Duration(days: 2)); + if (group.lastMessageSend!.isBefore(twoDaysAgo) || + group.lastMessageReceived!.isBefore(twoDaysAgo)) { + flameCounter = 0; + } + } + + var lastMessageSend = const Value.absent(); + var lastMessageReceived = const Value.absent(); + var lastFlameCounterChange = const Value.absent(); + + final now = clock.now(); + final startOfToday = DateTime(now.year, now.month, now.day); + + if (group.lastFlameCounterChange == null || + group.lastFlameCounterChange!.isBefore(startOfToday)) { + // last flame update was yesterday. check if it can be updated. + var updateFlame = false; + if (received) { + if (group.lastMessageSend != null && + group.lastMessageSend!.isAfter(startOfToday)) { + // today a message was already send -> update flame + updateFlame = true; + } + } else if (group.lastMessageReceived != null && + group.lastMessageReceived!.isAfter(startOfToday)) { + // today a message was already received -> update flame + updateFlame = true; + } + if (updateFlame) { + flameCounter += 1; + if (group.lastFlameCounterChange == null || + group.lastFlameCounterChange!.isBefore(timestamp)) { + // only update if the timestamp is newer + lastFlameCounterChange = Value(timestamp); + } + // Overwrite max flame counter either the current is bigger or the the max flame counter is older then 4 days + if (flameCounter >= maxFlameCounter || + maxFlameCounterFrom == null || + maxFlameCounterFrom + .isBefore(clock.now().subtract(const Duration(days: 5)))) { + maxFlameCounter = flameCounter; + maxFlameCounterFrom = clock.now(); + } + } + } + + if (received) { + if (group.lastMessageReceived == null || + group.lastMessageReceived!.isBefore(timestamp)) { + lastMessageReceived = Value(timestamp); + } + } else { + if (group.lastMessageSend == null || + group.lastMessageSend!.isBefore(timestamp)) { + lastMessageSend = Value(timestamp); + } + } + + await twonlyDB.groupsDao.updateGroup( + groupId, + GroupsCompanion( + totalMediaCounter: Value(totalMediaCounter), + lastFlameCounterChange: lastFlameCounterChange, + lastMessageReceived: lastMessageReceived, + lastMessageSend: lastMessageSend, + flameCounter: Value(flameCounter), + maxFlameCounter: Value(maxFlameCounter), + maxFlameCounterFrom: Value(maxFlameCounterFrom), + ), + ); +} + +bool isItPossibleToRestoreFlames(Group group) { + final flameCounter = getFlameCounterFromGroup(group); + return group.maxFlameCounter > 2 && + flameCounter < group.maxFlameCounter && + group.maxFlameCounterFrom! + .isAfter(clock.now().subtract(const Duration(days: 5))); +} diff --git a/lib/src/services/group.services.dart b/lib/src/services/group.services.dart index 3d81340..e9c8697 100644 --- a/lib/src/services/group.services.dart +++ b/lib/src/services/group.services.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math'; +import 'package:clock/clock.dart'; import 'package:collection/collection.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; @@ -158,7 +159,7 @@ Future fetchMissingGroupPublicKey() async { if (member.lastMessage == null) continue; // only request if the users has send a message in the last two days. if (member.lastMessage! - .isAfter(DateTime.now().subtract(const Duration(days: 2)))) { + .isAfter(clock.now().subtract(const Duration(days: 2)))) { await sendCipherText( member.contactId, EncryptedContent( diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index ea93aba..22952cb 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:path/path.dart'; import 'package:twonly/globals.dart'; @@ -61,7 +62,7 @@ class MediaFileService { // delete = true; // do not overwrite a previous delete = false // this is just to make it easier to understand :) } else if (message.openedAt! - .isAfter(DateTime.now().subtract(const Duration(days: 2)))) { + .isAfter(clock.now().subtract(const Duration(days: 2)))) { // In case the image was opened, but send with unlimited time or no authentication. if (message.senderId == null) { delete = false; diff --git a/lib/src/services/notifications/pushkeys.notifications.dart b/lib/src/services/notifications/pushkeys.notifications.dart index c3555aa..062b87c 100644 --- a/lib/src/services/notifications/pushkeys.notifications.dart +++ b/lib/src/services/notifications/pushkeys.notifications.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math'; +import 'package:clock/clock.dart'; import 'package:collection/collection.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; @@ -46,7 +47,7 @@ Future setupNotificationWithUsers({ final random = Random.secure(); - final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts(); + final contacts = await twonlyDB.contactsDao.getAllContacts(); for (final contact in contacts) { final pushUser = pushUsers.firstWhereOrNull((x) => x.userId == contact.userId); @@ -54,7 +55,7 @@ Future setupNotificationWithUsers({ if (pushUser != null && pushUser.pushKeys.isNotEmpty) { // make it harder to predict the change of the key final timeBefore = - DateTime.now().subtract(Duration(days: 5 + random.nextInt(5))); + clock.now().subtract(Duration(days: 10 + random.nextInt(5))); final lastKey = pushUser.pushKeys.last; final createdAt = DateTime.fromMillisecondsSinceEpoch( lastKey.createdAtUnixTimestamp.toInt(), @@ -66,7 +67,7 @@ Future setupNotificationWithUsers({ final pushKey = PushKey( id: lastKey.id + random.nextInt(5), key: List.generate(32, (index) => random.nextInt(256)), - createdAtUnixTimestamp: Int64(DateTime.now().millisecondsSinceEpoch), + createdAtUnixTimestamp: Int64(clock.now().millisecondsSinceEpoch), ); await sendNewPushKey(contact.userId, pushKey); // only store a maximum of two keys @@ -86,7 +87,7 @@ Future setupNotificationWithUsers({ final pushKey = PushKey( id: Int64(1), key: List.generate(32, (index) => random.nextInt(256)), - createdAtUnixTimestamp: Int64(DateTime.now().millisecondsSinceEpoch), + createdAtUnixTimestamp: Int64(clock.now().millisecondsSinceEpoch), ); await sendNewPushKey(contact.userId, pushKey); pushUsers.add( @@ -173,7 +174,7 @@ Future handleNewPushKey(int fromUserId, int keyId, List key) async { PushKey( id: Int64(keyId), key: key, - createdAtUnixTimestamp: Int64(DateTime.now().millisecondsSinceEpoch), + createdAtUnixTimestamp: Int64(clock.now().millisecondsSinceEpoch), ), ); @@ -341,7 +342,7 @@ Future encryptPushNotification( final createdAt = DateTime.fromMillisecondsSinceEpoch( pushUser.pushKeys.last.createdAtUnixTimestamp.toInt(), ); - final timeBefore = DateTime.now().subtract(const Duration(days: 8)); + final timeBefore = clock.now().subtract(const Duration(days: 8)); if (createdAt.isBefore(timeBefore)) { await requestNewPushKeysForUser(toUserId); } diff --git a/lib/src/services/signal/identity.signal.dart b/lib/src/services/signal/identity.signal.dart index f2e3129..aecd4a7 100644 --- a/lib/src/services/signal/identity.signal.dart +++ b/lib/src/services/signal/identity.signal.dart @@ -1,5 +1,5 @@ import 'dart:convert'; - +import 'package:clock/clock.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:twonly/globals.dart'; @@ -21,8 +21,7 @@ Future getSignalIdentityKeyPair() async { // It then checks if it should update a new session key Future signalHandleNewServerConnection() async { if (gUser.signalLastSignedPreKeyUpdated != null) { - final fortyEightHoursAgo = - DateTime.now().subtract(const Duration(hours: 48)); + final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48)); final isYoungerThan48Hours = (gUser.signalLastSignedPreKeyUpdated!).isAfter(fortyEightHoursAgo); if (isYoungerThan48Hours) { @@ -36,7 +35,7 @@ Future signalHandleNewServerConnection() async { return; } await updateUserdata((user) { - user.signalLastSignedPreKeyUpdated = DateTime.now(); + user.signalLastSignedPreKeyUpdated = clock.now(); return user; }); final res = await apiService.updateSignedPreKey( diff --git a/lib/src/services/signal/prekeys.signal.dart b/lib/src/services/signal/prekeys.signal.dart index 4823eea..9b27e04 100644 --- a/lib/src/services/signal/prekeys.signal.dart +++ b/lib/src/services/signal/prekeys.signal.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; @@ -22,17 +23,17 @@ class OtherPreKeys { } Mutex requestNewKeys = Mutex(); -DateTime lastPreKeyRequest = DateTime.now().subtract(const Duration(hours: 1)); +DateTime lastPreKeyRequest = clock.now().subtract(const Duration(hours: 1)); DateTime lastSignedPreKeyRequest = - DateTime.now().subtract(const Duration(hours: 1)); + clock.now().subtract(const Duration(hours: 1)); Future requestNewPrekeysForContact(int contactId) async { if (lastPreKeyRequest - .isAfter(DateTime.now().subtract(const Duration(seconds: 60)))) { + .isAfter(clock.now().subtract(const Duration(seconds: 60)))) { return; } Log.info('[PREKEY] Requesting new PREKEYS for $contactId'); - lastPreKeyRequest = DateTime.now(); + lastPreKeyRequest = clock.now(); await requestNewKeys.protect(() async { final otherKeys = await apiService.getPreKeysByUserId(contactId); if (otherKeys != null) { @@ -65,11 +66,11 @@ Future getPreKeyByContactId(int contactId) async { Future requestNewSignedPreKeyForContact(int contactId) async { if (lastSignedPreKeyRequest - .isAfter(DateTime.now().subtract(const Duration(seconds: 60)))) { + .isAfter(clock.now().subtract(const Duration(seconds: 60)))) { Log.info('last signed pre request was 60s before'); return; } - lastSignedPreKeyRequest = DateTime.now(); + lastSignedPreKeyRequest = clock.now(); await requestNewKeys.protect(() async { final signedPreKey = await apiService.getSignedKeyByUserId(contactId); if (signedPreKey != null) { @@ -96,8 +97,7 @@ Future getSignedPreKeyByContactId( await twonlyDB.signalDao.getSignedPreKeyByContactId(contactId); if (signedPreKey != null) { - final fortyEightHoursAgo = - DateTime.now().subtract(const Duration(hours: 48)); + final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48)); final isOlderThan48Hours = signedPreKey.createdAt.isBefore(fortyEightHoursAgo); if (isOlderThan48Hours) { diff --git a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart index 640e9cd..c6a8379 100644 --- a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart +++ b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; +import 'package:clock/clock.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:drift/drift.dart'; @@ -34,8 +35,7 @@ Future performTwonlySafeBackup({bool force = false}) async { final lastUpdateTime = gUser.twonlySafeBackup!.lastBackupDone; if (!force && lastUpdateTime != null) { - if (lastUpdateTime - .isAfter(DateTime.now().subtract(const Duration(days: 1)))) { + if (lastUpdateTime.isAfter(clock.now().subtract(const Duration(days: 1)))) { return; } } @@ -118,7 +118,7 @@ Future performTwonlySafeBackup({bool force = false}) async { if (gUser.twonlySafeBackup!.lastBackupDone == null || gUser.twonlySafeBackup!.lastBackupDone! - .isAfter(DateTime.now().subtract(const Duration(days: 90)))) { + .isAfter(clock.now().subtract(const Duration(days: 90)))) { force = true; } @@ -190,7 +190,7 @@ Future performTwonlySafeBackup({bool force = false}) async { Log.info('Starting upload from twonly Backup.'); await updateUserdata((user) { user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.pending; - user.twonlySafeBackup!.lastBackupDone = DateTime.now(); + user.twonlySafeBackup!.lastBackupDone = clock.now(); user.twonlySafeBackup!.lastBackupSize = encryptedBackupBytes.length; return user; }); diff --git a/lib/src/utils/avatars.dart b/lib/src/utils/avatars.dart index c92ebd4..08a903e 100644 --- a/lib/src/utils/avatars.dart +++ b/lib/src/utils/avatars.dart @@ -7,7 +7,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/utils/misc.dart'; Future createPushAvatars() async { - final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts(); + final contacts = await twonlyDB.contactsDao.getAllContacts(); for (final contact in contacts) { if (contact.avatarSvgCompressed == null) continue; diff --git a/lib/src/utils/log.dart b/lib/src/utils/log.dart index a46a3bf..1f8677a 100644 --- a/lib/src/utils/log.dart +++ b/lib/src/utils/log.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:clock/clock.dart'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:mutex/mutex.dart'; @@ -96,7 +97,7 @@ Future _writeLogToFile(LogRecord record) async { // Prepare the log message final logMessage = - '${DateTime.now().toString().split(".")[0]} ${record.level.name} [twonly] ${record.loggerName} > ${record.message}\n'; + '${clock.now().toString().split(".")[0]} ${record.level.name} [twonly] ${record.loggerName} > ${record.message}\n'; await writeToLogGuard.protect(() async { // Append the log message to the file diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 8a5186c..031bad0 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:math'; +import 'package:clock/clock.dart'; import 'package:convert/convert.dart'; import 'package:crypto/crypto.dart'; import 'package:flutter/material.dart'; @@ -197,7 +198,7 @@ bool isDarkMode(BuildContext context) { } bool isToday(DateTime lastImageSend) { - final now = DateTime.now(); + final now = clock.now(); return lastImageSend.year == now.year && lastImageSend.month == now.month && lastImageSend.day == now.day; @@ -235,7 +236,7 @@ String formatDateTime(BuildContext context, DateTime? dateTime) { if (dateTime == null) { return 'Never'; } - final now = DateTime.now(); + final now = clock.now(); final difference = now.difference(dateTime); final date = DateFormat.yMd(Localizations.localeOf(context).toLanguageTag()) diff --git a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart index 1009857..9600cd5 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; import 'package:camera/camera.dart'; +import 'package:clock/clock.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -165,7 +166,7 @@ class _CameraPreviewViewState extends State { DateTime? _videoRecordingStarted; Timer? _videoRecordingTimer; - DateTime _currentTime = DateTime.now(); + DateTime _currentTime = clock.now(); final GlobalKey keyTriggerButton = GlobalKey(); final GlobalKey navigatorKey = GlobalKey(); @@ -519,7 +520,7 @@ class _CameraPreviewViewState extends State { _videoRecordingTimer = Timer.periodic(const Duration(milliseconds: 15), (timer) { setState(() { - _currentTime = DateTime.now(); + _currentTime = clock.now(); }); if (_videoRecordingStarted != null && _currentTime.difference(_videoRecordingStarted!).inSeconds >= @@ -530,7 +531,7 @@ class _CameraPreviewViewState extends State { } }); setState(() { - _videoRecordingStarted = DateTime.now(); + _videoRecordingStarted = clock.now(); _isVideoRecording = true; }); } on CameraException catch (e) { diff --git a/lib/src/views/camera/camera_preview_components/video_recording_time.dart b/lib/src/views/camera/camera_preview_components/video_recording_time.dart index bcbc11c..d8b5211 100644 --- a/lib/src/views/camera/camera_preview_components/video_recording_time.dart +++ b/lib/src/views/camera/camera_preview_components/video_recording_time.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; class VideoRecordingTimer extends StatelessWidget { @@ -12,7 +13,7 @@ class VideoRecordingTimer extends StatelessWidget { @override Widget build(BuildContext context) { if (videoRecordingStarted != null) { - final currentTime = DateTime.now(); + final currentTime = clock.now(); return Positioned( top: 50, left: 0, diff --git a/lib/src/views/camera/image_editor/layers/filters/datetime_filter.dart b/lib/src/views/camera/image_editor/layers/filters/datetime_filter.dart index 0382721..d85e8b7 100644 --- a/lib/src/views/camera/image_editor/layers/filters/datetime_filter.dart +++ b/lib/src/views/camera/image_editor/layers/filters/datetime_filter.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:twonly/src/views/camera/image_editor/layers/filter_layer.dart'; @@ -9,8 +10,8 @@ class DateTimeFilter extends StatelessWidget { @override Widget build(BuildContext context) { - final currentTime = DateFormat('HH:mm').format(DateTime.now()); - final currentDate = DateFormat('dd.MM.yyyy').format(DateTime.now()); + final currentTime = DateFormat('HH:mm').format(clock.now()); + final currentDate = DateFormat('dd.MM.yyyy').format(clock.now()); return FilterSkeleton( child: Positioned( bottom: 80, diff --git a/lib/src/views/camera/image_editor/layers/filters/location_filter.dart b/lib/src/views/camera/image_editor/layers/filters/location_filter.dart index d8ef27f..fa38367 100644 --- a/lib/src/views/camera/image_editor/layers/filters/location_filter.dart +++ b/lib/src/views/camera/image_editor/layers/filters/location_filter.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:clock/clock.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; @@ -133,7 +134,7 @@ Future> getStickerIndex() async { if (indexFile.existsSync() && kReleaseMode) { final lastModified = indexFile.lastModifiedSync(); - final difference = DateTime.now().difference(lastModified); + final difference = clock.now().difference(lastModified); final content = await indexFile.readAsString(); final jsonList = json.decode(content) as List; res = jsonList diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index 3f12c0e..60fdfa6 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:collection'; +import 'package:clock/clock.dart'; import 'package:drift/drift.dart' show Value; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -509,7 +510,7 @@ class _ShareImageEditorView extends State { final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( MediaFilesCompanion( type: Value(mediaService.mediaFile.type), - createdAt: Value(DateTime.now()), + createdAt: Value(clock.now()), stored: const Value(true), ), ); diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index 423725c..0c22e03 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -7,10 +7,10 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; -import 'package:twonly/src/database/daos/groups.dao.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/share_image_components/best_friends_selector.dart'; diff --git a/lib/src/views/chats/chat_list_components/last_message_time.dart b/lib/src/views/chats/chat_list_components/last_message_time.dart index 609cc24..0615cb3 100644 --- a/lib/src/views/chats/chat_list_components/last_message_time.dart +++ b/lib/src/views/chats/chat_list_components/last_message_time.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -28,12 +29,13 @@ class _LastMessageTimeState extends State { if (widget.message != null) { final lastAction = await twonlyDB.messagesDao .getLastMessageAction(widget.message!.messageId); - lastMessageInSeconds = DateTime.now() + lastMessageInSeconds = clock + .now() .difference(lastAction?.actionAt ?? widget.message!.createdAt) .inSeconds; } else if (widget.dateTime != null) { lastMessageInSeconds = - DateTime.now().difference(widget.dateTime!).inSeconds; + clock.now().difference(widget.dateTime!).inSeconds; } if (mounted) { setState(() { diff --git a/lib/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart b/lib/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart index 1d793e0..24900de 100644 --- a/lib/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart +++ b/lib/src/views/chats/chat_messages_components/entries/friendly_message_time.comp.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart' show DateFormat; @@ -51,7 +52,7 @@ class FriendlyMessageTime extends StatelessWidget { } String friendlyTime(BuildContext context, DateTime dt) { - final now = DateTime.now(); + final now = clock.now(); final diff = now.difference(dt); if (diff.inMinutes >= 0 && diff.inMinutes < 60) { diff --git a/lib/src/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/views/chats/chat_messages_components/message_context_menu.dart index 88903f8..5226345 100644 --- a/lib/src/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/views/chats/chat_messages_components/message_context_menu.dart @@ -1,5 +1,6 @@ // ignore_for_file: inference_failure_on_function_invocation +import 'package:clock/clock.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -113,7 +114,7 @@ class MessageContextMenu extends StatelessWidget { await twonlyDB.messagesDao.handleMessageDeletion( null, message.messageId, - DateTime.now(), + clock.now(), ); await sendCipherTextToGroup( message.groupId, @@ -203,7 +204,7 @@ Future editTextMessage(BuildContext context, Message message) async { if (newText != null && newText != message.content && newText != '') { - final timestamp = DateTime.now(); + final timestamp = clock.now(); await twonlyDB.messagesDao.handleTextEdit( null, diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index cb2afac..9544731 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:collection'; +import 'package:clock/clock.dart'; import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -291,12 +292,12 @@ class _MediaViewerViewState extends State { }).catchError(Log.error); } else { if (currentMediaLocal.mediaFile.displayLimitInMilliseconds != null) { - canBeSeenUntil = DateTime.now().add( - Duration( - milliseconds: - currentMediaLocal.mediaFile.displayLimitInMilliseconds!, - ), - ); + canBeSeenUntil = clock.now().add( + Duration( + milliseconds: + currentMediaLocal.mediaFile.displayLimitInMilliseconds!, + ), + ); timerRequired = true; } } @@ -314,7 +315,7 @@ class _MediaViewerViewState extends State { nextMediaTimer?.cancel(); progressTimer?.cancel(); if (canBeSeenUntil != null) { - nextMediaTimer = Timer(canBeSeenUntil!.difference(DateTime.now()), () { + nextMediaTimer = Timer(canBeSeenUntil!.difference(clock.now()), () { if (context.mounted) { nextMediaOrExit(); } @@ -326,7 +327,7 @@ class _MediaViewerViewState extends State { canBeSeenUntil == null) { return; } - final difference = canBeSeenUntil!.difference(DateTime.now()); + final difference = canBeSeenUntil!.difference(clock.now()); // Calculate the progress as a value between 0.0 and 1.0 progress = difference.inMilliseconds / (mediaFile.displayLimitInMilliseconds!); diff --git a/lib/src/views/components/max_flame_list_title.dart b/lib/src/views/components/max_flame_list_title.dart index 479b3e8..5029bbb 100644 --- a/lib/src/views/components/max_flame_list_title.dart +++ b/lib/src/views/components/max_flame_list_title.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:clock/clock.dart'; import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; @@ -23,39 +24,22 @@ class MaxFlameListTitle extends StatefulWidget { } class _MaxFlameListTitleState extends State { - int _flameCounter = 0; - Group? _directChat; + Group? _group; late String _groupId; - - late StreamSubscription _flameCounterSub; late StreamSubscription _groupSub; @override void initState() { _groupId = getUUIDforDirectChat(widget.contactId, gUser.userId); - final stream = twonlyDB.groupsDao.watchFlameCounter(_groupId); - _flameCounterSub = stream.listen((counter) { - if (mounted) { - setState(() { - // in the watchFlameCounter a one is added, so remove this here - _flameCounter = counter - 1; - }); - } - }); - final stream2 = twonlyDB.groupsDao.watchGroup(_groupId); - _groupSub = stream2.listen((update) { - if (mounted) { - setState(() { - _directChat = update; - }); - } + final stream = twonlyDB.groupsDao.watchGroup(_groupId); + _groupSub = stream.listen((update) { + if (mounted) setState(() => _group = update); }); super.initState(); } @override void dispose() { - _flameCounterSub.cancel(); _groupSub.cancel(); super.dispose(); } @@ -73,13 +57,13 @@ class _MaxFlameListTitleState extends State { return; } Log.info( - 'Restoring flames from ${_directChat!.flameCounter} to ${_directChat!.maxFlameCounter}', + 'Restoring flames from ${_group!.flameCounter} to ${_group!.maxFlameCounter}', ); await twonlyDB.groupsDao.updateGroup( _groupId, GroupsCompanion( - flameCounter: Value(_directChat!.maxFlameCounter), - lastFlameCounterChange: Value(DateTime.now()), + flameCounter: Value(_group!.maxFlameCounter), + lastFlameCounterChange: Value(clock.now()), ), ); await syncFlameCounters(forceForGroup: _groupId); @@ -87,11 +71,7 @@ class _MaxFlameListTitleState extends State { @override Widget build(BuildContext context) { - if (_directChat == null || - _directChat!.maxFlameCounter <= 2 || - _flameCounter >= _directChat!.maxFlameCounter || - _directChat!.maxFlameCounterFrom! - .isBefore(DateTime.now().subtract(const Duration(days: 4)))) { + if (_group == null || !isItPossibleToRestoreFlames(_group!)) { return Container(); } return BetterListTile( @@ -102,7 +82,7 @@ class _MaxFlameListTitleState extends State { emoji: '🔥', ), ), - text: 'Restore your ${_directChat!.maxFlameCounter + 1} lost flames', + text: 'Restore your ${_group!.maxFlameCounter} lost flames', ); } } diff --git a/lib/src/views/memories/memories.view.dart b/lib/src/views/memories/memories.view.dart index edcbfa8..19357c9 100644 --- a/lib/src/views/memories/memories.view.dart +++ b/lib/src/views/memories/memories.view.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:twonly/globals.dart'; @@ -65,7 +66,7 @@ class MemoriesViewState extends State { var lastMonth = ''; galleryItems = []; - final now = DateTime.now(); + final now = clock.now(); for (final mediaFile in mediaFiles) { final mediaService = MediaFileService(mediaFile); diff --git a/lib/src/views/updates/62_database_migration.view.dart b/lib/src/views/updates/62_database_migration.view.dart index aac2625..58bdfc3 100644 --- a/lib/src/views/updates/62_database_migration.view.dart +++ b/lib/src/views/updates/62_database_migration.view.dart @@ -1,6 +1,7 @@ import 'dart:collection' show HashSet; import 'dart:convert'; import 'dart:io'; +import 'package:clock/clock.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; @@ -81,7 +82,7 @@ class _DatabaseMigrationViewState extends State { lastMessageSend: Value(oldContact.lastMessageSend), flameCounter: Value(oldContact.flameCounter), maxFlameCounter: Value(oldContact.flameCounter), - maxFlameCounterFrom: Value(DateTime.now()), + maxFlameCounterFrom: Value(clock.now()), ), ); } catch (e) { diff --git a/pubspec.lock b/pubspec.lock index ea1d1dc..7df1a10 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -266,7 +266,7 @@ packages: source: hosted version: "0.4.2" clock: - dependency: transitive + dependency: "direct main" description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b diff --git a/pubspec.yaml b/pubspec.yaml index 125a52c..4e3c46f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: web_socket_channel: ^3.0.1 convert: ^3.1.2 crypto: ^3.0.7 + clock: ^1.1.2 # Trusted publisher flutter.dev diff --git a/test/features/flame_counter_test.dart b/test/features/flame_counter_test.dart new file mode 100644 index 0000000..d5c43d8 --- /dev/null +++ b/test/features/flame_counter_test.dart @@ -0,0 +1,139 @@ +import 'package:clock/clock.dart'; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mutex/mutex.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/userdata.dart'; +import 'package:twonly/src/services/flame.service.dart'; + +Future expectFlame(DateTime time, String groupId, int counter) async { + await withClock( + Clock.fixed(time), + () async { + final group = (await twonlyDB.groupsDao.getGroup(groupId))!; + expect( + getFlameCounterFromGroup(group), + counter, + reason: StackTrace.current.toString(), + ); + }, + ); +} + +void main() { + final mutex = Mutex(); + var usedUserIds = 0; + + Future getAndCreateUserId() async { + return mutex.protect(() async { + final userId = usedUserIds += 1; + await twonlyDB.contactsDao.insertContact( + ContactsCompanion(userId: Value(userId), username: Value('$userId')), + ); + return userId; + }); + } + + setUp(() async { + twonlyDB = TwonlyDB.forTesting( + DatabaseConnection( + NativeDatabase.memory(), + // Recommended for widget tests to avoid test errors. + closeStreamsSynchronously: true, + ), + ); + + gUser = UserData( + userId: 0x133337, + username: 'test_user', + displayName: 'Test User', + subscriptionPlan: 'Free', + )..appVersion = 62; + }); + + test('test flame counter', () async { + final contactId = await getAndCreateUserId(); + final contact = (await twonlyDB.contactsDao.getContactById(contactId))!; + await twonlyDB.groupsDao.createNewDirectChat( + contactId, + GroupsCompanion( + groupName: Value( + getContactDisplayName(contact), + ), + ), + ); + + final group = (await twonlyDB.groupsDao.getDirectChat(contactId))!; + + await expectFlame(DateTime(2026, 2, 3, 16), group.groupId, 0); + + await withClock( + Clock.fixed(DateTime(2026, 2, 2, 16)), + () async { + await incFlameCounter(group.groupId, true, DateTime(2026, 2, 2, 10)); + await incFlameCounter(group.groupId, false, DateTime(2026, 2, 2, 15)); + }, + ); + + await expectFlame(DateTime(2026, 2, 2, 18), group.groupId, 1); + await expectFlame(DateTime(2026, 2, 3, 16), group.groupId, 1); + await expectFlame(DateTime(2026, 2, 4, 16), group.groupId, 1); + await expectFlame(DateTime(2026, 2, 5, 16), group.groupId, 0); + + await withClock( + Clock.fixed(DateTime(2026, 2, 5, 16)), + () async { + await incFlameCounter(group.groupId, true, DateTime(2026, 2, 5, 17)); + await incFlameCounter(group.groupId, false, DateTime(2026, 2, 5, 18)); + }, + ); + + await expectFlame(DateTime(2026, 2, 5, 19), group.groupId, 1); + + await withClock( + Clock.fixed(DateTime(2026, 2, 6, 1)), + () async { + await incFlameCounter(group.groupId, true, DateTime(2026, 2, 6, 1)); + await incFlameCounter(group.groupId, false, DateTime(2026, 2, 6, 2)); + }, + ); + + await expectFlame(DateTime(2026, 2, 6, 19), group.groupId, 2); + await expectFlame(DateTime(2026, 3, 1, 19), group.groupId, 0); + + for (var i = 1; i <= 20; i++) { + await withClock( + Clock.fixed(DateTime(2026, 3, i, 1)), + () async { + await incFlameCounter(group.groupId, true, DateTime(2026, 3, i, 2)); + await incFlameCounter(group.groupId, false, DateTime(2026, 3, i, 3)); + }, + ); + } + + await expectFlame(DateTime(2026, 3, 20, 19), group.groupId, 20); + await expectFlame(DateTime(2026, 3, 23, 19), group.groupId, 0); + + await withClock( + Clock.fixed(DateTime(2026, 3, 24, 19)), + () async { + final group2 = (await twonlyDB.groupsDao.getGroup(group.groupId))!; + expect(isItPossibleToRestoreFlames(group2), true); + }, + ); + await withClock( + Clock.fixed(DateTime(2026, 3, 25, 19)), + () async { + final group2 = (await twonlyDB.groupsDao.getGroup(group.groupId))!; + expect(isItPossibleToRestoreFlames(group2), false); + }, + ); + }); + + tearDown(() async { + await twonlyDB.close(); + }); +}