diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 45e09d0f..8b5348be 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -153,18 +153,25 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { ); final groupIds = entry.value; - await (delete(messages)..where( - (m) => - m.groupId.isIn(groupIds) & - ((m.mediaStored.equals(true) & - m.isDeletedFromSender.equals(true)) | - m.mediaStored.equals(false)) & - // Only remove the message when ALL members have seen it. Otherwise the receipt will also be deleted which could cause issues in case a member opens the image later.. - (m.openedByAll.isSmallerThanValue(deletionTime) | - (m.isDeletedFromSender.equals(true) & - m.createdAt.isSmallerThanValue(deletionTime))), - )) - .go(); + final deletedCount = + await (delete(messages)..where( + (m) => + m.groupId.isIn(groupIds) & + ((m.mediaStored.equals(true) & + m.isDeletedFromSender.equals(true)) | + m.mediaStored.equals(false)) & + // Only remove the message when ALL members have seen it. Otherwise the receipt will also be deleted which could cause issues in case a member opens the image later.. + (m.openedByAll.isSmallerThanValue(deletionTime) | + (m.isDeletedFromSender.equals(true) & + m.createdAt.isSmallerThanValue(deletionTime))), + )) + .go(); + + if (deletedCount > 0) { + Log.info( + 'Deleted $deletedCount messages for groups $groupIds due to retention policy.', + ); + } } } @@ -266,30 +273,48 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { actionTimestamp = msg.createdAt; } - await into(messageActions).insertOnConflictUpdate( - MessageActionsCompanion( - messageId: Value(messageId), - contactId: contactId, - type: const Value(MessageActionType.openedAt), - actionAt: Value(actionTimestamp), - ), - ); + final ts = actionTimestamp; + await transaction(() async { + await into(messageActions).insertOnConflictUpdate( + MessageActionsCompanion( + messageId: Value(messageId), + contactId: contactId, + type: const Value(MessageActionType.openedAt), + actionAt: Value(ts), + ), + ); + + final isOpenedByAll = await haveAllMembers( + messageId, + MessageActionType.openedAt, + ); + await (update( + messages, + )..where((tbl) => tbl.messageId.equals(messageId))).write( + MessagesCompanion( + openedAt: Value(ts), + openedByAll: Value(isOpenedByAll ? ts : null), + ), + ); + }); + + // Read-back verification: confirm the write was persisted. + final verified = await getMessageById(messageId).getSingleOrNull(); + if (verified != null && verified.openedAt == null) { + Log.warn( + 'handleMessagesOpened read-back failed for $messageId, retrying', + ); + await (update( + messages, + )..where((tbl) => tbl.messageId.equals(messageId))).write( + MessagesCompanion( + openedAt: Value(actionTimestamp), + ), + ); + } - final isOpenedByAll = await haveAllMembers( - messageId, - MessageActionType.openedAt, - ); - final rowsUpdated = - await (update( - messages, - )..where((tbl) => tbl.messageId.equals(messageId))).write( - MessagesCompanion( - openedAt: Value(actionTimestamp), - openedByAll: Value(isOpenedByAll ? actionTimestamp : null), - ), - ); Log.info( - 'handleMessagesOpened updated $rowsUpdated rows for message $messageId', + 'handleMessagesOpened completed for message $messageId', ); } catch (e) { Log.error('handleMessagesOpened failed for $messageId: $e'); @@ -302,18 +327,20 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { String messageId, DateTime timestamp, ) async { - await into(messageActions).insertOnConflictUpdate( - MessageActionsCompanion( - messageId: Value(messageId), - contactId: Value(contactId), - type: const Value(MessageActionType.ackByServerAt), - actionAt: Value(timestamp), - ), - ); - await twonlyDB.messagesDao.updateMessageId( - messageId, - MessagesCompanion(ackByServer: Value(timestamp)), - ); + await transaction(() async { + await into(messageActions).insertOnConflictUpdate( + MessageActionsCompanion( + messageId: Value(messageId), + contactId: Value(contactId), + type: const Value(MessageActionType.ackByServerAt), + actionAt: Value(timestamp), + ), + ); + await twonlyDB.messagesDao.updateMessageId( + messageId, + MessagesCompanion(ackByServer: Value(timestamp)), + ); + }); } Future haveAllMembers( @@ -347,18 +374,20 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { String messageId, MessagesCompanion updatedValues, ) async { - await (update( + final count = await (update( messages, )..where((c) => c.messageId.equals(messageId))).write(updatedValues); + Log.info('Updated $count message(s) with messageId $messageId'); } Future updateMessagesByMediaId( String mediaId, MessagesCompanion updatedValues, - ) { - return (update( + ) async { + final count = await (update( messages, )..where((c) => c.mediaId.equals(mediaId))).write(updatedValues); + Log.info('Updated $count message(s) with mediaId $mediaId'); } Future insertMessage(MessagesCompanion message) async { diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index 5ab35682..e1566f0c 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -1,4 +1,3 @@ -import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart' show DriftNativeOptions, driftDatabase; @@ -246,38 +245,4 @@ class TwonlyDB extends _$TwonlyDB { Log.info('Table: $tableName, Size: $tableSize bytes'); } } - - Future deleteDataForTwonlySafe() async { - await (delete(messages)..where( - (t) => - (t.mediaStored.equals(false) & - t.isDeletedFromSender.equals(false)), - )) - .go(); - await update(messages).write( - const MessagesCompanion( - downloadToken: Value(null), - ), - ); - await (delete(mediaFiles)..where( - (t) => (t.stored.equals(false)), - )) - .go(); - await delete(receipts).go(); - await delete(receivedReceipts).go(); - await update(contacts).write( - const ContactsCompanion( - avatarSvgCompressed: Value(null), - senderProfileCounter: Value(0), - ), - ); - await (delete(signalPreKeyStores)..where( - (t) => (t.createdAt.isSmallerThanValue( - clock.now().subtract( - const Duration(days: 25), - ), - )), - )) - .go(); - } } diff --git a/lib/src/services/api/mediafiles/upload.api.dart b/lib/src/services/api/mediafiles/upload.api.dart index bce9db1d..4fac6607 100644 --- a/lib/src/services/api/mediafiles/upload.api.dart +++ b/lib/src/services/api/mediafiles/upload.api.dart @@ -64,20 +64,6 @@ Future reuploadMediaFiles() async { } } - if (receipt.retryCount >= 2) { - // After two retries, change the receiptId. This addresses a bug where the receiver received the message and marked it as received, but the app was closed before the message was fully processed. Because the receipt was already stored, subsequent retries were detected as duplicates and rejected. - final oldReceiptId = receipt.receiptId; - final updatedReceipt = await twonlyDB.receiptsDao.rotateReceiptId( - oldReceiptId, - ); - if (updatedReceipt == null) continue; - - Log.info( - 'Changed receiptId $oldReceiptId to ${updatedReceipt.receiptId} as retryCount is ${receipt.retryCount}', - ); - receipt = updatedReceipt; - } - var messageId = receipt.messageId; if (receipt.messageId == null) { Log.info('Message not in receipt. Loading it from the content.');