add flame counter test
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled

This commit is contained in:
otsmr 2025-12-30 19:58:32 +01:00
parent ad0ef841cc
commit 585f577f89
42 changed files with 407 additions and 247 deletions

View file

@ -99,7 +99,7 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
.watchSingleOrNull(); .watchSingleOrNull();
} }
Future<List<Contact>> getAllNotBlockedContacts() { Future<List<Contact>> getAllContacts() {
return select(contacts).get(); return select(contacts).get();
} }

View file

@ -3,6 +3,7 @@ import 'package:hashlib/random.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.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/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -278,89 +279,6 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
return query.map((row) => row.readTable(groups)).getSingleOrNull(); return query.map((row) => row.readTable(groups)).getSingleOrNull();
} }
Future<void> 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<DateTime?>.absent();
var lastMessageReceived = const Value<DateTime?>.absent();
var lastFlameCounterChange = const Value<DateTime?>.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<int> watchSumTotalMediaCounter() { Stream<int> watchSumTotalMediaCounter() {
final query = selectOnly(groups) final query = selectOnly(groups)
..addColumns([groups.totalMediaCounter.sum()]); ..addColumns([groups.totalMediaCounter.sum()]);
@ -383,23 +301,3 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
.write(GroupsCompanion(lastMessageExchange: Value(newLastMessage))); .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;
}
}

View file

@ -1,3 +1,4 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
@ -110,11 +111,11 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
final allGroups = await select(groups).get(); final allGroups = await select(groups).get();
for (final group in allGroups) { for (final group in allGroups) {
final deletionTime = DateTime.now().subtract( final deletionTime = clock.now().subtract(
Duration( Duration(
milliseconds: group.deleteMessagesAfterMilliseconds, milliseconds: group.deleteMessagesAfterMilliseconds,
), ),
); );
await (delete(messages) await (delete(messages)
..where( ..where(
(m) => (m) =>
@ -150,7 +151,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
// t.messageOtherId.isNull() & // t.messageOtherId.isNull() &
// t.errorWhileSending.equals(false) & // t.errorWhileSending.equals(false) &
// t.sendAt.isBiggerThanValue( // t.sendAt.isBiggerThanValue(
// DateTime.now().subtract(const Duration(minutes: 10)), // clock.now().subtract(const Duration(minutes: 10)),
// ), // ),
// )) // ))
// .get(); // .get();
@ -178,7 +179,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
// } // }
Future<void> openedAllTextMessages(String groupId) { Future<void> openedAllTextMessages(String groupId) {
final updates = MessagesCompanion(openedAt: Value(DateTime.now())); final updates = MessagesCompanion(openedAt: Value(clock.now()));
return (update(messages) return (update(messages)
..where( ..where(
(t) => (t) =>
@ -272,12 +273,12 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
// Directly show as message opened as soon as one person has opened it // Directly show as message opened as soon as one person has opened it
final openedByAll = final openedByAll =
await haveAllMembers(messageId, MessageActionType.openedAt) await haveAllMembers(messageId, MessageActionType.openedAt)
? DateTime.now() ? clock.now()
: null; : null;
await twonlyDB.messagesDao.updateMessageId( await twonlyDB.messagesDao.updateMessageId(
messageId, messageId,
MessagesCompanion( MessagesCompanion(
openedAt: Value(DateTime.now()), openedAt: Value(clock.now()),
openedByAll: Value(openedByAll), openedByAll: Value(openedByAll),
), ),
); );
@ -298,7 +299,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
); );
await twonlyDB.messagesDao.updateMessageId( await twonlyDB.messagesDao.updateMessageId(
messageId, messageId,
MessagesCompanion(ackByServer: Value(DateTime.now())), MessagesCompanion(ackByServer: Value(clock.now())),
); );
} }
@ -378,7 +379,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(
message.groupId.value, message.groupId.value,
GroupsCompanion( GroupsCompanion(
lastMessageExchange: Value(DateTime.now()), lastMessageExchange: Value(clock.now()),
archived: const Value(false), archived: const Value(false),
deletedContent: const Value(false), deletedContent: const Value(false),
), ),
@ -389,7 +390,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
message.groupId.value, message.groupId.value,
message.senderId.value!, message.senderId.value!,
GroupMembersCompanion( GroupMembersCompanion(
lastMessage: Value(DateTime.now()), lastMessage: Value(clock.now()),
), ),
); );
} }

View file

@ -1,3 +1,4 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart';
@ -81,12 +82,12 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
} }
Future<List<Receipt>> getReceiptsForRetransmission() async { Future<List<Receipt>> getReceiptsForRetransmission() async {
final markedRetriesTime = DateTime.now().subtract( final markedRetriesTime = clock.now().subtract(
const Duration( const Duration(
// give the server time to transmit all messages to the client // give the server time to transmit all messages to the client
seconds: 20, seconds: 20,
), ),
); );
return (select(receipts) return (select(receipts)
..where( ..where(
(t) => (t) =>
@ -111,7 +112,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
Future<void> markMessagesForRetry(int contactId) async { Future<void> markMessagesForRetry(int contactId) async {
await (update(receipts)..where((c) => c.contactId.equals(contactId))).write( await (update(receipts)..where((c) => c.contactId.equals(contactId))).write(
ReceiptsCompanion( ReceiptsCompanion(
markForRetry: Value(DateTime.now()), markForRetry: Value(clock.now()),
), ),
); );
} }

View file

@ -1,3 +1,4 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/signal_contact_prekey.table.dart'; import 'package:twonly/src/database/tables/signal_contact_prekey.table.dart';
@ -107,9 +108,9 @@ class SignalDao extends DatabaseAccessor<TwonlyDB> with _$SignalDaoMixin {
await (delete(signalContactPreKeys) await (delete(signalContactPreKeys)
..where( ..where(
(t) => (t.createdAt.isSmallerThanValue( (t) => (t.createdAt.isSmallerThanValue(
DateTime.now().subtract( clock.now().subtract(
const Duration(days: 100), const Duration(days: 100),
), ),
)), )),
)) ))
.go(); .go();
@ -117,9 +118,9 @@ class SignalDao extends DatabaseAccessor<TwonlyDB> with _$SignalDaoMixin {
await (delete(twonlyDB.signalPreKeyStores) await (delete(twonlyDB.signalPreKeyStores)
..where( ..where(
(t) => (t.createdAt.isSmallerThanValue( (t) => (t.createdAt.isSmallerThanValue(
DateTime.now().subtract( clock.now().subtract(
const Duration(days: 365), const Duration(days: 365),
), ),
)), )),
)) ))
.go(); .go();

View file

@ -1,3 +1,4 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart' import 'package:drift_flutter/drift_flutter.dart'
show DriftNativeOptions, driftDatabase; show DriftNativeOptions, driftDatabase;
@ -160,9 +161,9 @@ class TwonlyDB extends _$TwonlyDB {
await (delete(signalPreKeyStores) await (delete(signalPreKeyStores)
..where( ..where(
(t) => (t.createdAt.isSmallerThanValue( (t) => (t.createdAt.isSmallerThanValue(
DateTime.now().subtract( clock.now().subtract(
const Duration(days: 25), const Duration(days: 25),
), ),
)), )),
)) ))
.go(); .go();

View file

@ -1,3 +1,4 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart' import 'package:drift_flutter/drift_flutter.dart'
show DriftNativeOptions, driftDatabase; show DriftNativeOptions, driftDatabase;
@ -191,9 +192,9 @@ class TwonlyDatabaseOld extends _$TwonlyDatabaseOld {
await (delete(signalPreKeyStores) await (delete(signalPreKeyStores)
..where( ..where(
(t) => (t.createdAt.isSmallerThanValue( (t) => (t.createdAt.isSmallerThanValue(
DateTime.now().subtract( clock.now().subtract(
const Duration(days: 25), const Duration(days: 25),
), ),
)), )),
)) ))
.go(); .go();

View file

@ -7,6 +7,7 @@ import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:clock/clock.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
@ -206,7 +207,7 @@ class ApiService {
} }
Future<server.ServerToClient?> _waitForResponse(Int64 seq) async { Future<server.ServerToClient?> _waitForResponse(Int64 seq) async {
final startTime = DateTime.now(); final startTime = clock.now();
const timeout = Duration(seconds: 60); const timeout = Duration(seconds: 60);
@ -216,7 +217,7 @@ class ApiService {
messagesV0.remove(seq); messagesV0.remove(seq);
return tmp; return tmp;
} }
if (DateTime.now().difference(startTime) > timeout) { if (clock.now().difference(startTime) > timeout) {
Log.warn('Timeout for message $seq'); Log.warn('Timeout for message $seq');
return null; return null;
} }

View file

@ -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/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/services/api/utils.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/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -114,7 +115,7 @@ Future<void> handleMedia(
await twonlyDB.groupsDao await twonlyDB.groupsDao
.increaseLastMessageExchange(groupId, fromTimestamp(media.timestamp)); .increaseLastMessageExchange(groupId, fromTimestamp(media.timestamp));
Log.info('Inserted a new media message with ID: ${message.messageId}'); Log.info('Inserted a new media message with ID: ${message.messageId}');
await twonlyDB.groupsDao.incFlameCounter( await incFlameCounter(
message.groupId, message.groupId,
true, true,
fromTimestamp(media.timestamp), fromTimestamp(media.timestamp),

View file

@ -1,10 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:clock/clock.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.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/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/utils/log.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<void> handlePushKey( Future<void> handlePushKey(
int contactId, int contactId,
@ -14,8 +15,8 @@ Future<void> handlePushKey(
case EncryptedContent_PushKeys_Type.REQUEST: case EncryptedContent_PushKeys_Type.REQUEST:
Log.info('Got a pushkey request from $contactId'); Log.info('Got a pushkey request from $contactId');
if (lastPushKeyRequest if (lastPushKeyRequest
.isBefore(DateTime.now().subtract(const Duration(seconds: 60)))) { .isBefore(clock.now().subtract(const Duration(seconds: 60)))) {
lastPushKeyRequest = DateTime.now(); lastPushKeyRequest = clock.now();
unawaited(setupNotificationWithUsers(forceContact: contactId)); unawaited(setupNotificationWithUsers(forceContact: contactId));
} }

View file

@ -1,3 +1,4 @@
import 'package:clock/clock.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -17,7 +18,6 @@ Future<void> handleReaction(
); );
if (!reaction.remove) { if (!reaction.remove) {
await twonlyDB.groupsDao await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
.increaseLastMessageExchange(groupId, DateTime.now());
} }
} }

View file

@ -1,3 +1,4 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart';
@ -26,7 +27,7 @@ Future<void> handleTextMessage(
textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null, textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null,
), ),
createdAt: Value(fromTimestamp(textMessage.timestamp)), createdAt: Value(fromTimestamp(textMessage.timestamp)),
ackByServer: Value(DateTime.now()), ackByServer: Value(clock.now()),
), ),
); );
await twonlyDB.groupsDao.increaseLastMessageExchange( await twonlyDB.groupsDao.increaseLastMessageExchange(

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
@ -92,7 +93,7 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
await twonlyDB.messagesDao.handleMessageAckByServer( await twonlyDB.messagesDao.handleMessageAckByServer(
contact.contactId, contact.contactId,
message.messageId, message.messageId,
DateTime.now(), clock.now(),
); );
} }
} }

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:clock/clock.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_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/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/mediafiles/media_background.service.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/api/messages.dart';
import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -101,8 +103,7 @@ Future<void> insertMediaFileInMessagesTable(
type: const Value(MessageType.media), type: const Value(MessageType.media),
), ),
); );
await twonlyDB.groupsDao await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
.increaseLastMessageExchange(groupId, DateTime.now());
if (message != null) { if (message != null) {
// de-archive contact when sending a new message // de-archive contact when sending a new message
await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(
@ -207,11 +208,7 @@ Future<void> _createUploadRequest(MediaFileService media) async {
await twonlyDB.groupsDao.getGroupNonLeftMembers(message.groupId); await twonlyDB.groupsDao.getGroupNonLeftMembers(message.groupId);
if (media.mediaFile.reuploadRequestedBy == null) { if (media.mediaFile.reuploadRequestedBy == null) {
await twonlyDB.groupsDao.incFlameCounter( await incFlameCounter(message.groupId, false, message.createdAt);
message.groupId,
false,
message.createdAt,
);
} }
for (final groupMember in groupMembers) { for (final groupMember in groupMembers) {

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
@ -131,7 +132,7 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
await twonlyDB.messagesDao.handleMessageAckByServer( await twonlyDB.messagesDao.handleMessageAckByServer(
receipt.contactId, receipt.contactId,
receipt.messageId!, receipt.messageId!,
DateTime.now(), clock.now(),
); );
} }
if (!receipt.contactWillSendsReceipt) { if (!receipt.contactWillSendsReceipt) {
@ -140,9 +141,9 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
await twonlyDB.receiptsDao.updateReceipt( await twonlyDB.receiptsDao.updateReceipt(
receiptId, receiptId,
ReceiptsCompanion( ReceiptsCompanion(
ackByServerAt: Value(DateTime.now()), ackByServerAt: Value(clock.now()),
retryCount: Value(receipt.retryCount + 1), retryCount: Value(receipt.retryCount + 1),
lastRetry: Value(DateTime.now()), lastRetry: Value(clock.now()),
markForRetry: const Value(null), markForRetry: const Value(null),
), ),
); );
@ -210,7 +211,7 @@ Future<void> sendCipherTextToGroup(
}) async { }) async {
final groupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(groupId); final groupMembers = await twonlyDB.groupsDao.getGroupNonLeftMembers(groupId);
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, DateTime.now()); await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
encryptedContent.groupId = groupId; encryptedContent.groupId = groupId;
@ -242,7 +243,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
contactId: Value(contactId), contactId: Value(contactId),
message: Value(response.writeToBuffer()), message: Value(response.writeToBuffer()),
messageId: Value(messageId), messageId: Value(messageId),
ackByServerAt: Value(onlyReturnEncryptedData ? DateTime.now() : null), ackByServerAt: Value(onlyReturnEncryptedData ? clock.now() : null),
), ),
); );
@ -268,7 +269,7 @@ Future<void> notifyContactAboutOpeningMessage(
} }
Log.info('Opened messages: $messageOtherIds'); Log.info('Opened messages: $messageOtherIds');
final actionAt = DateTime.now(); final actionAt = clock.now();
await sendCipherText( await sendCipherText(
contactId, contactId,

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
@ -60,7 +61,7 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
await apiService.sendResponse(ClientToServer()..v0 = v0); 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(); Mutex protectReceiptCheck = Mutex();

View file

@ -1,8 +1,8 @@
import 'package:clock/clock.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:twonly/globals.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/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
@ -32,10 +32,10 @@ Future<void> 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 // only sync when flame counter is higher three or when they are bestFriends
if (flameCounter < 1 && bestFriend.groupId != group.groupId) continue; if (flameCounter <= 2 && bestFriend.groupId != group.groupId) continue;
final groupMembers = final groupMembers =
await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId); await twonlyDB.groupsDao.getGroupNonLeftMembers(group.groupId);
@ -59,8 +59,125 @@ Future<void> syncFlameCounters({String? forceForGroup}) async {
await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(
group.groupId, group.groupId,
GroupsCompanion( 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<void> 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<DateTime?>.absent();
var lastMessageReceived = const Value<DateTime?>.absent();
var lastFlameCounterChange = const Value<DateTime?>.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)));
}

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:clock/clock.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
@ -158,7 +159,7 @@ Future<void> fetchMissingGroupPublicKey() async {
if (member.lastMessage == null) continue; if (member.lastMessage == null) continue;
// only request if the users has send a message in the last two days. // only request if the users has send a message in the last two days.
if (member.lastMessage! if (member.lastMessage!
.isAfter(DateTime.now().subtract(const Duration(days: 2)))) { .isAfter(clock.now().subtract(const Duration(days: 2)))) {
await sendCipherText( await sendCipherText(
member.contactId, member.contactId,
EncryptedContent( EncryptedContent(

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
@ -61,7 +62,7 @@ class MediaFileService {
// delete = true; // do not overwrite a previous delete = false // delete = true; // do not overwrite a previous delete = false
// this is just to make it easier to understand :) // this is just to make it easier to understand :)
} else if (message.openedAt! } 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. // In case the image was opened, but send with unlimited time or no authentication.
if (message.senderId == null) { if (message.senderId == null) {
delete = false; delete = false;

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:clock/clock.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
@ -46,7 +47,7 @@ Future<void> setupNotificationWithUsers({
final random = Random.secure(); final random = Random.secure();
final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts(); final contacts = await twonlyDB.contactsDao.getAllContacts();
for (final contact in contacts) { for (final contact in contacts) {
final pushUser = final pushUser =
pushUsers.firstWhereOrNull((x) => x.userId == contact.userId); pushUsers.firstWhereOrNull((x) => x.userId == contact.userId);
@ -54,7 +55,7 @@ Future<void> setupNotificationWithUsers({
if (pushUser != null && pushUser.pushKeys.isNotEmpty) { if (pushUser != null && pushUser.pushKeys.isNotEmpty) {
// make it harder to predict the change of the key // make it harder to predict the change of the key
final timeBefore = 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 lastKey = pushUser.pushKeys.last;
final createdAt = DateTime.fromMillisecondsSinceEpoch( final createdAt = DateTime.fromMillisecondsSinceEpoch(
lastKey.createdAtUnixTimestamp.toInt(), lastKey.createdAtUnixTimestamp.toInt(),
@ -66,7 +67,7 @@ Future<void> setupNotificationWithUsers({
final pushKey = PushKey( final pushKey = PushKey(
id: lastKey.id + random.nextInt(5), id: lastKey.id + random.nextInt(5),
key: List<int>.generate(32, (index) => random.nextInt(256)), key: List<int>.generate(32, (index) => random.nextInt(256)),
createdAtUnixTimestamp: Int64(DateTime.now().millisecondsSinceEpoch), createdAtUnixTimestamp: Int64(clock.now().millisecondsSinceEpoch),
); );
await sendNewPushKey(contact.userId, pushKey); await sendNewPushKey(contact.userId, pushKey);
// only store a maximum of two keys // only store a maximum of two keys
@ -86,7 +87,7 @@ Future<void> setupNotificationWithUsers({
final pushKey = PushKey( final pushKey = PushKey(
id: Int64(1), id: Int64(1),
key: List<int>.generate(32, (index) => random.nextInt(256)), key: List<int>.generate(32, (index) => random.nextInt(256)),
createdAtUnixTimestamp: Int64(DateTime.now().millisecondsSinceEpoch), createdAtUnixTimestamp: Int64(clock.now().millisecondsSinceEpoch),
); );
await sendNewPushKey(contact.userId, pushKey); await sendNewPushKey(contact.userId, pushKey);
pushUsers.add( pushUsers.add(
@ -173,7 +174,7 @@ Future<void> handleNewPushKey(int fromUserId, int keyId, List<int> key) async {
PushKey( PushKey(
id: Int64(keyId), id: Int64(keyId),
key: key, key: key,
createdAtUnixTimestamp: Int64(DateTime.now().millisecondsSinceEpoch), createdAtUnixTimestamp: Int64(clock.now().millisecondsSinceEpoch),
), ),
); );
@ -341,7 +342,7 @@ Future<Uint8List?> encryptPushNotification(
final createdAt = DateTime.fromMillisecondsSinceEpoch( final createdAt = DateTime.fromMillisecondsSinceEpoch(
pushUser.pushKeys.last.createdAtUnixTimestamp.toInt(), 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)) { if (createdAt.isBefore(timeBefore)) {
await requestNewPushKeysForUser(toUserId); await requestNewPushKeysForUser(toUserId);
} }

View file

@ -1,5 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:clock/clock.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
@ -21,8 +21,7 @@ Future<IdentityKeyPair?> getSignalIdentityKeyPair() async {
// It then checks if it should update a new session key // It then checks if it should update a new session key
Future<void> signalHandleNewServerConnection() async { Future<void> signalHandleNewServerConnection() async {
if (gUser.signalLastSignedPreKeyUpdated != null) { if (gUser.signalLastSignedPreKeyUpdated != null) {
final fortyEightHoursAgo = final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48));
DateTime.now().subtract(const Duration(hours: 48));
final isYoungerThan48Hours = final isYoungerThan48Hours =
(gUser.signalLastSignedPreKeyUpdated!).isAfter(fortyEightHoursAgo); (gUser.signalLastSignedPreKeyUpdated!).isAfter(fortyEightHoursAgo);
if (isYoungerThan48Hours) { if (isYoungerThan48Hours) {
@ -36,7 +35,7 @@ Future<void> signalHandleNewServerConnection() async {
return; return;
} }
await updateUserdata((user) { await updateUserdata((user) {
user.signalLastSignedPreKeyUpdated = DateTime.now(); user.signalLastSignedPreKeyUpdated = clock.now();
return user; return user;
}); });
final res = await apiService.updateSignedPreKey( final res = await apiService.updateSignedPreKey(

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
@ -22,17 +23,17 @@ class OtherPreKeys {
} }
Mutex requestNewKeys = Mutex(); Mutex requestNewKeys = Mutex();
DateTime lastPreKeyRequest = DateTime.now().subtract(const Duration(hours: 1)); DateTime lastPreKeyRequest = clock.now().subtract(const Duration(hours: 1));
DateTime lastSignedPreKeyRequest = DateTime lastSignedPreKeyRequest =
DateTime.now().subtract(const Duration(hours: 1)); clock.now().subtract(const Duration(hours: 1));
Future<void> requestNewPrekeysForContact(int contactId) async { Future<void> requestNewPrekeysForContact(int contactId) async {
if (lastPreKeyRequest if (lastPreKeyRequest
.isAfter(DateTime.now().subtract(const Duration(seconds: 60)))) { .isAfter(clock.now().subtract(const Duration(seconds: 60)))) {
return; return;
} }
Log.info('[PREKEY] Requesting new PREKEYS for $contactId'); Log.info('[PREKEY] Requesting new PREKEYS for $contactId');
lastPreKeyRequest = DateTime.now(); lastPreKeyRequest = clock.now();
await requestNewKeys.protect(() async { await requestNewKeys.protect(() async {
final otherKeys = await apiService.getPreKeysByUserId(contactId); final otherKeys = await apiService.getPreKeysByUserId(contactId);
if (otherKeys != null) { if (otherKeys != null) {
@ -65,11 +66,11 @@ Future<SignalContactPreKey?> getPreKeyByContactId(int contactId) async {
Future<void> requestNewSignedPreKeyForContact(int contactId) async { Future<void> requestNewSignedPreKeyForContact(int contactId) async {
if (lastSignedPreKeyRequest 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'); Log.info('last signed pre request was 60s before');
return; return;
} }
lastSignedPreKeyRequest = DateTime.now(); lastSignedPreKeyRequest = clock.now();
await requestNewKeys.protect(() async { await requestNewKeys.protect(() async {
final signedPreKey = await apiService.getSignedKeyByUserId(contactId); final signedPreKey = await apiService.getSignedKeyByUserId(contactId);
if (signedPreKey != null) { if (signedPreKey != null) {
@ -96,8 +97,7 @@ Future<SignalContactSignedPreKey?> getSignedPreKeyByContactId(
await twonlyDB.signalDao.getSignedPreKeyByContactId(contactId); await twonlyDB.signalDao.getSignedPreKeyByContactId(contactId);
if (signedPreKey != null) { if (signedPreKey != null) {
final fortyEightHoursAgo = final fortyEightHoursAgo = clock.now().subtract(const Duration(hours: 48));
DateTime.now().subtract(const Duration(hours: 48));
final isOlderThan48Hours = final isOlderThan48Hours =
signedPreKey.createdAt.isBefore(fortyEightHoursAgo); signedPreKey.createdAt.isBefore(fortyEightHoursAgo);
if (isOlderThan48Hours) { if (isOlderThan48Hours) {

View file

@ -3,6 +3,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:clock/clock.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
@ -34,8 +35,7 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
final lastUpdateTime = gUser.twonlySafeBackup!.lastBackupDone; final lastUpdateTime = gUser.twonlySafeBackup!.lastBackupDone;
if (!force && lastUpdateTime != null) { if (!force && lastUpdateTime != null) {
if (lastUpdateTime if (lastUpdateTime.isAfter(clock.now().subtract(const Duration(days: 1)))) {
.isAfter(DateTime.now().subtract(const Duration(days: 1)))) {
return; return;
} }
} }
@ -118,7 +118,7 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
if (gUser.twonlySafeBackup!.lastBackupDone == null || if (gUser.twonlySafeBackup!.lastBackupDone == null ||
gUser.twonlySafeBackup!.lastBackupDone! gUser.twonlySafeBackup!.lastBackupDone!
.isAfter(DateTime.now().subtract(const Duration(days: 90)))) { .isAfter(clock.now().subtract(const Duration(days: 90)))) {
force = true; force = true;
} }
@ -190,7 +190,7 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
Log.info('Starting upload from twonly Backup.'); Log.info('Starting upload from twonly Backup.');
await updateUserdata((user) { await updateUserdata((user) {
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.pending; user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.pending;
user.twonlySafeBackup!.lastBackupDone = DateTime.now(); user.twonlySafeBackup!.lastBackupDone = clock.now();
user.twonlySafeBackup!.lastBackupSize = encryptedBackupBytes.length; user.twonlySafeBackup!.lastBackupSize = encryptedBackupBytes.length;
return user; return user;
}); });

View file

@ -7,7 +7,7 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
Future<void> createPushAvatars() async { Future<void> createPushAvatars() async {
final contacts = await twonlyDB.contactsDao.getAllNotBlockedContacts(); final contacts = await twonlyDB.contactsDao.getAllContacts();
for (final contact in contacts) { for (final contact in contacts) {
if (contact.avatarSvgCompressed == null) continue; if (contact.avatarSvgCompressed == null) continue;

View file

@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:clock/clock.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
@ -96,7 +97,7 @@ Future<void> _writeLogToFile(LogRecord record) async {
// Prepare the log message // Prepare the log message
final logMessage = 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 { await writeToLogGuard.protect(() async {
// Append the log message to the file // Append the log message to the file

View file

@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:clock/clock.dart';
import 'package:convert/convert.dart'; import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -197,7 +198,7 @@ bool isDarkMode(BuildContext context) {
} }
bool isToday(DateTime lastImageSend) { bool isToday(DateTime lastImageSend) {
final now = DateTime.now(); final now = clock.now();
return lastImageSend.year == now.year && return lastImageSend.year == now.year &&
lastImageSend.month == now.month && lastImageSend.month == now.month &&
lastImageSend.day == now.day; lastImageSend.day == now.day;
@ -235,7 +236,7 @@ String formatDateTime(BuildContext context, DateTime? dateTime) {
if (dateTime == null) { if (dateTime == null) {
return 'Never'; return 'Never';
} }
final now = DateTime.now(); final now = clock.now();
final difference = now.difference(dateTime); final difference = now.difference(dateTime);
final date = DateFormat.yMd(Localizations.localeOf(context).toLanguageTag()) final date = DateFormat.yMd(Localizations.localeOf(context).toLanguageTag())

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:clock/clock.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -165,7 +166,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
DateTime? _videoRecordingStarted; DateTime? _videoRecordingStarted;
Timer? _videoRecordingTimer; Timer? _videoRecordingTimer;
DateTime _currentTime = DateTime.now(); DateTime _currentTime = clock.now();
final GlobalKey keyTriggerButton = GlobalKey(); final GlobalKey keyTriggerButton = GlobalKey();
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@ -519,7 +520,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
_videoRecordingTimer = _videoRecordingTimer =
Timer.periodic(const Duration(milliseconds: 15), (timer) { Timer.periodic(const Duration(milliseconds: 15), (timer) {
setState(() { setState(() {
_currentTime = DateTime.now(); _currentTime = clock.now();
}); });
if (_videoRecordingStarted != null && if (_videoRecordingStarted != null &&
_currentTime.difference(_videoRecordingStarted!).inSeconds >= _currentTime.difference(_videoRecordingStarted!).inSeconds >=
@ -530,7 +531,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
}); });
setState(() { setState(() {
_videoRecordingStarted = DateTime.now(); _videoRecordingStarted = clock.now();
_isVideoRecording = true; _isVideoRecording = true;
}); });
} on CameraException catch (e) { } on CameraException catch (e) {

View file

@ -1,3 +1,4 @@
import 'package:clock/clock.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class VideoRecordingTimer extends StatelessWidget { class VideoRecordingTimer extends StatelessWidget {
@ -12,7 +13,7 @@ class VideoRecordingTimer extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (videoRecordingStarted != null) { if (videoRecordingStarted != null) {
final currentTime = DateTime.now(); final currentTime = clock.now();
return Positioned( return Positioned(
top: 50, top: 50,
left: 0, left: 0,

View file

@ -1,3 +1,4 @@
import 'package:clock/clock.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:twonly/src/views/camera/image_editor/layers/filter_layer.dart'; import 'package:twonly/src/views/camera/image_editor/layers/filter_layer.dart';
@ -9,8 +10,8 @@ class DateTimeFilter extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final currentTime = DateFormat('HH:mm').format(DateTime.now()); final currentTime = DateFormat('HH:mm').format(clock.now());
final currentDate = DateFormat('dd.MM.yyyy').format(DateTime.now()); final currentDate = DateFormat('dd.MM.yyyy').format(clock.now());
return FilterSkeleton( return FilterSkeleton(
child: Positioned( child: Positioned(
bottom: 80, bottom: 80,

View file

@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:clock/clock.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@ -133,7 +134,7 @@ Future<List<Sticker>> getStickerIndex() async {
if (indexFile.existsSync() && kReleaseMode) { if (indexFile.existsSync() && kReleaseMode) {
final lastModified = indexFile.lastModifiedSync(); final lastModified = indexFile.lastModifiedSync();
final difference = DateTime.now().difference(lastModified); final difference = clock.now().difference(lastModified);
final content = await indexFile.readAsString(); final content = await indexFile.readAsString();
final jsonList = json.decode(content) as List; final jsonList = json.decode(content) as List;
res = jsonList res = jsonList

View file

@ -2,6 +2,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -509,7 +510,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
MediaFilesCompanion( MediaFilesCompanion(
type: Value(mediaService.mediaFile.type), type: Value(mediaService.mediaFile.type),
createdAt: Value(DateTime.now()), createdAt: Value(clock.now()),
stored: const Value(true), stored: const Value(true),
), ),
); );

View file

@ -7,10 +7,10 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.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/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.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/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/share_image_components/best_friends_selector.dart'; import 'package:twonly/src/views/camera/share_image_components/best_friends_selector.dart';

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:clock/clock.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
@ -28,12 +29,13 @@ class _LastMessageTimeState extends State<LastMessageTime> {
if (widget.message != null) { if (widget.message != null) {
final lastAction = await twonlyDB.messagesDao final lastAction = await twonlyDB.messagesDao
.getLastMessageAction(widget.message!.messageId); .getLastMessageAction(widget.message!.messageId);
lastMessageInSeconds = DateTime.now() lastMessageInSeconds = clock
.now()
.difference(lastAction?.actionAt ?? widget.message!.createdAt) .difference(lastAction?.actionAt ?? widget.message!.createdAt)
.inSeconds; .inSeconds;
} else if (widget.dateTime != null) { } else if (widget.dateTime != null) {
lastMessageInSeconds = lastMessageInSeconds =
DateTime.now().difference(widget.dateTime!).inSeconds; clock.now().difference(widget.dateTime!).inSeconds;
} }
if (mounted) { if (mounted) {
setState(() { setState(() {

View file

@ -1,3 +1,4 @@
import 'package:clock/clock.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart' show DateFormat; import 'package:intl/intl.dart' show DateFormat;
@ -51,7 +52,7 @@ class FriendlyMessageTime extends StatelessWidget {
} }
String friendlyTime(BuildContext context, DateTime dt) { String friendlyTime(BuildContext context, DateTime dt) {
final now = DateTime.now(); final now = clock.now();
final diff = now.difference(dt); final diff = now.difference(dt);
if (diff.inMinutes >= 0 && diff.inMinutes < 60) { if (diff.inMinutes >= 0 && diff.inMinutes < 60) {

View file

@ -1,5 +1,6 @@
// ignore_for_file: inference_failure_on_function_invocation // ignore_for_file: inference_failure_on_function_invocation
import 'package:clock/clock.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -113,7 +114,7 @@ class MessageContextMenu extends StatelessWidget {
await twonlyDB.messagesDao.handleMessageDeletion( await twonlyDB.messagesDao.handleMessageDeletion(
null, null,
message.messageId, message.messageId,
DateTime.now(), clock.now(),
); );
await sendCipherTextToGroup( await sendCipherTextToGroup(
message.groupId, message.groupId,
@ -203,7 +204,7 @@ Future<void> editTextMessage(BuildContext context, Message message) async {
if (newText != null && if (newText != null &&
newText != message.content && newText != message.content &&
newText != '') { newText != '') {
final timestamp = DateTime.now(); final timestamp = clock.now();
await twonlyDB.messagesDao.handleTextEdit( await twonlyDB.messagesDao.handleTextEdit(
null, null,

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
@ -291,12 +292,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
}).catchError(Log.error); }).catchError(Log.error);
} else { } else {
if (currentMediaLocal.mediaFile.displayLimitInMilliseconds != null) { if (currentMediaLocal.mediaFile.displayLimitInMilliseconds != null) {
canBeSeenUntil = DateTime.now().add( canBeSeenUntil = clock.now().add(
Duration( Duration(
milliseconds: milliseconds:
currentMediaLocal.mediaFile.displayLimitInMilliseconds!, currentMediaLocal.mediaFile.displayLimitInMilliseconds!,
), ),
); );
timerRequired = true; timerRequired = true;
} }
} }
@ -314,7 +315,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
nextMediaTimer?.cancel(); nextMediaTimer?.cancel();
progressTimer?.cancel(); progressTimer?.cancel();
if (canBeSeenUntil != null) { if (canBeSeenUntil != null) {
nextMediaTimer = Timer(canBeSeenUntil!.difference(DateTime.now()), () { nextMediaTimer = Timer(canBeSeenUntil!.difference(clock.now()), () {
if (context.mounted) { if (context.mounted) {
nextMediaOrExit(); nextMediaOrExit();
} }
@ -326,7 +327,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
canBeSeenUntil == null) { canBeSeenUntil == null) {
return; 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 // Calculate the progress as a value between 0.0 and 1.0
progress = progress =
difference.inMilliseconds / (mediaFile.displayLimitInMilliseconds!); difference.inMilliseconds / (mediaFile.displayLimitInMilliseconds!);

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
@ -23,39 +24,22 @@ class MaxFlameListTitle extends StatefulWidget {
} }
class _MaxFlameListTitleState extends State<MaxFlameListTitle> { class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
int _flameCounter = 0; Group? _group;
Group? _directChat;
late String _groupId; late String _groupId;
late StreamSubscription<int> _flameCounterSub;
late StreamSubscription<Group?> _groupSub; late StreamSubscription<Group?> _groupSub;
@override @override
void initState() { void initState() {
_groupId = getUUIDforDirectChat(widget.contactId, gUser.userId); _groupId = getUUIDforDirectChat(widget.contactId, gUser.userId);
final stream = twonlyDB.groupsDao.watchFlameCounter(_groupId); final stream = twonlyDB.groupsDao.watchGroup(_groupId);
_flameCounterSub = stream.listen((counter) { _groupSub = stream.listen((update) {
if (mounted) { if (mounted) setState(() => _group = update);
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;
});
}
}); });
super.initState(); super.initState();
} }
@override @override
void dispose() { void dispose() {
_flameCounterSub.cancel();
_groupSub.cancel(); _groupSub.cancel();
super.dispose(); super.dispose();
} }
@ -73,13 +57,13 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
return; return;
} }
Log.info( Log.info(
'Restoring flames from ${_directChat!.flameCounter} to ${_directChat!.maxFlameCounter}', 'Restoring flames from ${_group!.flameCounter} to ${_group!.maxFlameCounter}',
); );
await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(
_groupId, _groupId,
GroupsCompanion( GroupsCompanion(
flameCounter: Value(_directChat!.maxFlameCounter), flameCounter: Value(_group!.maxFlameCounter),
lastFlameCounterChange: Value(DateTime.now()), lastFlameCounterChange: Value(clock.now()),
), ),
); );
await syncFlameCounters(forceForGroup: _groupId); await syncFlameCounters(forceForGroup: _groupId);
@ -87,11 +71,7 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_directChat == null || if (_group == null || !isItPossibleToRestoreFlames(_group!)) {
_directChat!.maxFlameCounter <= 2 ||
_flameCounter >= _directChat!.maxFlameCounter ||
_directChat!.maxFlameCounterFrom!
.isBefore(DateTime.now().subtract(const Duration(days: 4)))) {
return Container(); return Container();
} }
return BetterListTile( return BetterListTile(
@ -102,7 +82,7 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
emoji: '🔥', emoji: '🔥',
), ),
), ),
text: 'Restore your ${_directChat!.maxFlameCounter + 1} lost flames', text: 'Restore your ${_group!.maxFlameCounter} lost flames',
); );
} }
} }

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:clock/clock.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
@ -65,7 +66,7 @@ class MemoriesViewState extends State<MemoriesView> {
var lastMonth = ''; var lastMonth = '';
galleryItems = []; galleryItems = [];
final now = DateTime.now(); final now = clock.now();
for (final mediaFile in mediaFiles) { for (final mediaFile in mediaFiles) {
final mediaService = MediaFileService(mediaFile); final mediaService = MediaFileService(mediaFile);

View file

@ -1,6 +1,7 @@
import 'dart:collection' show HashSet; import 'dart:collection' show HashSet;
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:clock/clock.dart';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -81,7 +82,7 @@ class _DatabaseMigrationViewState extends State<DatabaseMigrationView> {
lastMessageSend: Value(oldContact.lastMessageSend), lastMessageSend: Value(oldContact.lastMessageSend),
flameCounter: Value(oldContact.flameCounter), flameCounter: Value(oldContact.flameCounter),
maxFlameCounter: Value(oldContact.flameCounter), maxFlameCounter: Value(oldContact.flameCounter),
maxFlameCounterFrom: Value(DateTime.now()), maxFlameCounterFrom: Value(clock.now()),
), ),
); );
} catch (e) { } catch (e) {

View file

@ -266,7 +266,7 @@ packages:
source: hosted source: hosted
version: "0.4.2" version: "0.4.2"
clock: clock:
dependency: transitive dependency: "direct main"
description: description:
name: clock name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b

View file

@ -25,6 +25,7 @@ dependencies:
web_socket_channel: ^3.0.1 web_socket_channel: ^3.0.1
convert: ^3.1.2 convert: ^3.1.2
crypto: ^3.0.7 crypto: ^3.0.7
clock: ^1.1.2
# Trusted publisher flutter.dev # Trusted publisher flutter.dev

View file

@ -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<void> 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<int> getAndCreateUserId() async {
return mutex.protect<int>(() 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();
});
}