diff --git a/.gitignore b/.gitignore index 9d085fd2..5451128a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ .history .svn/ .swiftpm/ +*.sqlite +*.sqlite-shm +*.sqlite-wal migrate_working_dir/ # IntelliJ related diff --git a/CHANGELOG.md b/CHANGELOG.md index 22fa3acd..3ed082a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - New: Tutorial on how to use zoom. - New: Manage storage view. - Improved: Media thumbnails for faster loading. +- Fix: Some message where not marked as opened. ## 0.2.12 diff --git a/lib/globals.dart b/lib/globals.dart index f3bd47b7..90bc7deb 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -32,6 +32,5 @@ class AppState { static bool isInBackgroundTask = false; static bool allowErrorTrackingViaSentry = false; static bool gotMessageFromServer = false; - static int latestAppVersionId = 115; - + static int latestAppVersionId = 116; } diff --git a/lib/main.dart b/lib/main.dart index 31213e24..a4ed4ed5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,6 @@ import 'dart:async'; -import 'dart:convert'; -import 'package:drift/drift.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:intl/intl.dart'; import 'package:mutex/mutex.dart'; import 'package:provider/provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -15,33 +11,24 @@ import 'package:twonly/core/frb_generated.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/callbacks/callbacks.dart'; -import 'package:twonly/src/constants/secure_storage.keys.dart'; -import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart' - show getSignalSignedPreKeyStoreOld; -import 'package:twonly/src/database/tables/contacts.table.dart'; -import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/signal_identity.model.dart'; import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/image_editor.provider.dart'; import 'package:twonly/src/providers/purchases.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; -import 'package:twonly/src/services/api/mediafiles/download.api.dart'; import 'package:twonly/src/services/api/mediafiles/media_background.api.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/services/background/callback_dispatcher.background.dart'; import 'package:twonly/src/services/backup.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/memories/memories.service.dart'; +import 'package:twonly/src/services/migrations.service.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; -import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user_discovery.service.dart'; import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/exclusive_access.utils.dart'; import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/secure_storage.dart'; import 'package:twonly/src/utils/startup_guard.dart'; -import 'package:twonly/src/visual/views/onboarding/setup.view.dart'; final _initMutex = Mutex(); @@ -167,144 +154,6 @@ void main() async { ); } -Future runMigrations() async { - if (userService.currentUser.appVersion < 90) { - // BUG: Requested media files for reupload where not reuploaded because the wrong state... - await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState(); - await UserService.update((u) => u.appVersion = 90); - } - - if (userService.currentUser.appVersion < 91) { - // BUG: Requested media files for reupload where not reuploaded because the wrong state... - await makeMigrationToVersion91(); - await UserService.update((u) => u.appVersion = 91); - } - - if (userService.currentUser.appVersion < 109) { - final contacts = await twonlyDB.contactsDao.getAllContacts(); - for (final contact in contacts) { - if (contact.verified) { - await twonlyDB.keyVerificationDao.addKeyVerification( - contact.userId, - VerificationType.migratedFromOldVersion, - ); - } - } - await UserService.update((u) { - u - ..appVersion = 109 - ..skipSetupPages = true; - if (u.avatarSvg == null) { - u.currentSetupPage = SetupPages.profile.name; - } else { - u.currentSetupPage = SetupPages.shareYourFriends.name; - } - }); - } - if (userService.currentUser.appVersion < 113) { - var migrationSuccess = true; - final signalIdentity = await SecureStorage.instance.read( - // ignore: deprecated_member_use_from_same_package - key: SecureStorageKeys.signalIdentity, - ); - - if (signalIdentity != null) { - try { - final decoded = jsonDecode(signalIdentity); - final identity = SignalIdentity.fromJson( - decoded as Map, - ); - - await RustKeyManager.importSignalIdentity( - identityKeyPairStructure: identity.identityKeyPairU8List, - registrationId: identity.registrationId, - signedPreKeyStore: await getSignalSignedPreKeyStoreOld(), - ); - Log.info('Importing signal identiy to the rust key manager'); - - // Clean up old keys after successful migration - await SecureStorage.instance.delete( - // ignore: deprecated_member_use_from_same_package - key: SecureStorageKeys.signalIdentity, - ); - await SecureStorage.instance.delete( - // ignore: deprecated_member_use_from_same_package - key: SecureStorageKeys.signalSignedPreKey, - ); - } catch (e) { - Log.error('Failed to migrate signal identity: $e'); - migrationSuccess = false; - } - } - - if (migrationSuccess) { - await UserService.update((u) { - u - ..appVersion = 113 - ..canUseLoginTokenForAuth = false - // As usernames changes where not considered in the old version force users - // to reenter there passwords. - // ignore: deprecated_member_use_from_same_package - ..twonlySafeBackup?.encryptionKey = [] - // ignore: deprecated_member_use_from_same_package - ..twonlySafeBackup?.backupId = []; - }); - } - } - if (userService.currentUser.appVersion < 114) { - final allMedia = await twonlyDB.mediaFilesDao - .select(twonlyDB.mediaFiles) - .get(); - for (final media in allMedia) { - if (media.createdAtMonth == null) { - final monthStr = DateFormat('MMMM yyyy').format(media.createdAt); - await twonlyDB.mediaFilesDao.updateMedia( - media.mediaId, - MediaFilesCompanion(createdAtMonth: Value(monthStr)), - ); - } - } - await UserService.update((u) => u.appVersion = 114); - } - - if (userService.currentUser.appVersion < 115) { - var migrationSuccess = true; - try { - final rustStore = await RustKeyManager.loadSignedPrekeys(); - for (final entry in rustStore.entries) { - final companion = SignalSignedPreKeyStoresCompanion( - signedPreKeyId: Value(entry.key), - signedPreKey: Value(entry.value), - ); - await twonlyDB - .into(twonlyDB.signalSignedPreKeyStores) - .insert( - companion, - mode: InsertMode.insertOrReplace, - ); - await RustKeyManager.removeSignedPrekey(signedPreKeyId: entry.key); - } - } catch (e) { - Log.error('Failed to migrate signed prekeys to Drift: $e'); - migrationSuccess = false; - } - if (migrationSuccess) { - await UserService.update((u) => u.appVersion = 115); - } - } - - if (kDebugMode) { - assert( - AppState.latestAppVersionId == 115, - 'Forgot to update the target version in runMigrations() after incrementing AppState.latestAppVersionId.', - ); - assert( - AppState.latestAppVersionId == userService.currentUser.appVersion, - "Migration incomplete: currentUser.appVersion (${userService.currentUser.appVersion}) does not match AppState.latestAppVersionId (${AppState.latestAppVersionId}). Ensure the user's appVersion is updated in the migration block.", - ); - } -} - Future postStartupTasks() async { Log.info('Post startup started.'); unawaited(MemoriesService.prewarmCache()); diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index ace070ea..1a1b5d15 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -249,41 +249,49 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { } Future handleMessagesOpened( - int contactId, + Value contactId, List messageIds, DateTime timestamp, ) async { - await batch((batch) async { - for (final messageId in messageIds) { - batch.insert( - messageActions, - MessageActionsCompanion( - messageId: Value(messageId), - contactId: Value(contactId), - type: const Value(MessageActionType.openedAt), - actionAt: Value(timestamp), - ), - mode: InsertMode.insertOrReplace, - ); - } + try { + await twonlyDB.batch((batch) async { + for (final messageId in messageIds) { + batch.insert( + messageActions, + MessageActionsCompanion( + messageId: Value(messageId), + contactId: contactId, + type: const Value(MessageActionType.openedAt), + actionAt: Value(timestamp), + ), + mode: InsertMode.insertOrReplace, + ); + } + }); + } catch (e) { + Log.error(e); + } - for (final messageId in messageIds) { + for (final messageId in messageIds) { + try { final isOpenedByAll = await haveAllMembers( messageId, MessageActionType.openedAt, ); final now = clock.now(); - batch.update( - twonlyDB.messages, + await (update( + messages, + )..where((tbl) => tbl.messageId.equals(messageId))).write( MessagesCompanion( openedAt: Value(now), openedByAll: Value(isOpenedByAll ? now : null), ), - where: (tbl) => tbl.messageId.equals(messageId), ); + } catch (e) { + Log.error(e); } - }); + } } Future handleMessageAckByServer( @@ -309,21 +317,27 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { String messageId, MessageActionType action, ) async { - final message = await twonlyDB.messagesDao - .getMessageById(messageId) - .getSingleOrNull(); - if (message == null) return true; - final members = await twonlyDB.groupsDao.getGroupNonLeftMembers( - message.groupId, - ); + try { + final message = await twonlyDB.messagesDao + .getMessageById(messageId) + .getSingleOrNull(); + if (message == null) return true; + final members = await twonlyDB.groupsDao.getGroupNonLeftMembers( + message.groupId, + ); - final actions = - await (select(messageActions)..where( - (t) => t.type.equals(action.name) & t.messageId.equals(messageId), - )) - .get(); + final actions = + await (select(messageActions)..where( + (t) => + t.type.equals(action.name) & t.messageId.equals(messageId), + )) + .get(); - return members.length == actions.length; + return members.length == actions.length; + } catch (e) { + Log.error(e); + return true; + } } Future updateMessageId( diff --git a/lib/src/services/api/client2client/messages.c2c.dart b/lib/src/services/api/client2client/messages.c2c.dart index 461846e0..4561687f 100644 --- a/lib/src/services/api/client2client/messages.c2c.dart +++ b/lib/src/services/api/client2client/messages.c2c.dart @@ -1,3 +1,4 @@ +import 'package:drift/drift.dart' show Value; import 'package:twonly/locator.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/utils.api.dart'; @@ -14,7 +15,7 @@ Future handleMessageUpdate( ); try { await twonlyDB.messagesDao.handleMessagesOpened( - contactId, + Value(contactId), messageUpdate.multipleTargetMessageIds, fromTimestamp(messageUpdate.timestamp), ); diff --git a/lib/src/services/migrations.service.dart b/lib/src/services/migrations.service.dart new file mode 100644 index 00000000..6e5497bb --- /dev/null +++ b/lib/src/services/migrations.service.dart @@ -0,0 +1,184 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:clock/clock.dart'; +import 'package:drift/drift.dart'; +import 'package:flutter/foundation.dart'; +import 'package:intl/intl.dart'; +import 'package:twonly/core/bridge/wrapper/key_manager.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/secure_storage.keys.dart'; +import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart' + show getSignalSignedPreKeyStoreOld; +import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/signal_identity.model.dart'; +import 'package:twonly/src/services/api/mediafiles/download.api.dart'; +import 'package:twonly/src/services/user.service.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/secure_storage.dart'; +import 'package:twonly/src/visual/views/onboarding/setup.view.dart'; + +Future runMigrations() async { + if (userService.currentUser.appVersion < 90) { + // BUG: Requested media files for reupload where not reuploaded because the wrong state... + await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState(); + await UserService.update((u) => u.appVersion = 90); + } + + if (userService.currentUser.appVersion < 91) { + // BUG: Requested media files for reupload where not reuploaded because the wrong state... + await makeMigrationToVersion91(); + await UserService.update((u) => u.appVersion = 91); + } + + if (userService.currentUser.appVersion < 109) { + final contacts = await twonlyDB.contactsDao.getAllContacts(); + for (final contact in contacts) { + if (contact.verified) { + await twonlyDB.keyVerificationDao.addKeyVerification( + contact.userId, + VerificationType.migratedFromOldVersion, + ); + } + } + await UserService.update((u) { + u + ..appVersion = 109 + ..skipSetupPages = true; + if (u.avatarSvg == null) { + u.currentSetupPage = SetupPages.profile.name; + } else { + u.currentSetupPage = SetupPages.shareYourFriends.name; + } + }); + } + if (userService.currentUser.appVersion < 113) { + var migrationSuccess = true; + final signalIdentity = await SecureStorage.instance.read( + // ignore: deprecated_member_use_from_same_package + key: SecureStorageKeys.signalIdentity, + ); + + if (signalIdentity != null) { + try { + final decoded = jsonDecode(signalIdentity); + final identity = SignalIdentity.fromJson( + decoded as Map, + ); + + await RustKeyManager.importSignalIdentity( + identityKeyPairStructure: identity.identityKeyPairU8List, + registrationId: identity.registrationId, + signedPreKeyStore: await getSignalSignedPreKeyStoreOld(), + ); + Log.info('Importing signal identify to the rust key manager'); + + // Clean up old keys after successful migration + await SecureStorage.instance.delete( + // ignore: deprecated_member_use_from_same_package + key: SecureStorageKeys.signalIdentity, + ); + await SecureStorage.instance.delete( + // ignore: deprecated_member_use_from_same_package + key: SecureStorageKeys.signalSignedPreKey, + ); + } catch (e) { + Log.error('Failed to migrate signal identity: $e'); + migrationSuccess = false; + } + } + + if (migrationSuccess) { + await UserService.update((u) { + u + ..appVersion = 113 + ..canUseLoginTokenForAuth = false + // As usernames changes where not considered in the old version force users + // to reenter there passwords. + // ignore: deprecated_member_use_from_same_package + ..twonlySafeBackup?.encryptionKey = [] + // ignore: deprecated_member_use_from_same_package + ..twonlySafeBackup?.backupId = []; + }); + } + } + if (userService.currentUser.appVersion < 114) { + final allMedia = await twonlyDB.mediaFilesDao + .select(twonlyDB.mediaFiles) + .get(); + for (final media in allMedia) { + if (media.createdAtMonth == null) { + final monthStr = DateFormat('MMMM yyyy').format(media.createdAt); + await twonlyDB.mediaFilesDao.updateMedia( + media.mediaId, + MediaFilesCompanion(createdAtMonth: Value(monthStr)), + ); + } + } + await UserService.update((u) => u.appVersion = 114); + } + + if (userService.currentUser.appVersion < 115) { + var migrationSuccess = true; + try { + final rustStore = await RustKeyManager.loadSignedPrekeys(); + for (final entry in rustStore.entries) { + final companion = SignalSignedPreKeyStoresCompanion( + signedPreKeyId: Value(entry.key), + signedPreKey: Value(entry.value), + ); + await twonlyDB + .into(twonlyDB.signalSignedPreKeyStores) + .insert( + companion, + mode: InsertMode.insertOrReplace, + ); + await RustKeyManager.removeSignedPrekey(signedPreKeyId: entry.key); + } + } catch (e) { + Log.error('Failed to migrate signed prekeys to Drift: $e'); + migrationSuccess = false; + } + if (migrationSuccess) { + await UserService.update((u) => u.appVersion = 115); + } + } + + if (userService.currentUser.appVersion < 116) { + // Because of a Bug in the handleMessagesOpened function, some messages where not marked as opened. So use the logs, + // to mark the files as opened. + final logs = await loadLogFile(); + final openedMessages = logs.split( + 'messages.c2c.dart:12 > Opened message [', + ); + for (final opened in openedMessages) { + final messageIds = opened.split(']'); + if (messageIds.isNotEmpty) { + final now = clock.now(); + for (final messageId in messageIds.first.split(',')) { + await (twonlyDB.update( + twonlyDB.messages, + )..where((tbl) => tbl.messageId.equals(messageId))).write( + MessagesCompanion( + openedAt: Value(now), + openedByAll: Value(now), + ), + ); + } + } + } + await UserService.update((u) => u.appVersion = 116); + } + + if (kDebugMode) { + assert( + AppState.latestAppVersionId == 116, + 'Forgot to update the target version in runMigrations() after incrementing AppState.latestAppVersionId.', + ); + assert( + AppState.latestAppVersionId == userService.currentUser.appVersion, + "Migration incomplete: currentUser.appVersion (${userService.currentUser.appVersion}) does not match AppState.latestAppVersionId (${AppState.latestAppVersionId}). Ensure the user's appVersion is updated in the migration block.", + ); + } +} diff --git a/lib/src/visual/views/chats/message_info.view.dart b/lib/src/visual/views/chats/message_info.view.dart index af83b06e..be6f5161 100644 --- a/lib/src/visual/views/chats/message_info.view.dart +++ b/lib/src/visual/views/chats/message_info.view.dart @@ -205,6 +205,8 @@ class _MessageInfoViewState extends State { Text( '${context.lang.received}: ${friendlyDateTime(context, widget.message.ackByServer!)}', ), + if (userService.currentUser.isDeveloper) + Text('ID: ${widget.message.messageId}'), if (messageHistory.isNotEmpty) ...[ const SizedBox(height: 10), const Divider(),